// Package mcinstall implementiert den minimalen MCInstall-Protocol-Flow // den wir für Cloud-Config-Manipulation brauchen. // // Wir nutzen go-ios's mcinstall.Connection für die exposed Calls // (GetCloudConfiguration, EscalateUnsupervised) und öffnen für // SetCloudConfiguration eine eigene Connection via ios.ConnectToService // + PlistCodec — weil go-ios's mcinstall.Connection.sendAndReceive nicht // public ist. // // Service-Name: "com.apple.mobile.MCInstall" // Protocol: Plist-encoded requests in {"RequestType": "...", ...} dicts. package mcinstall import ( "crypto/x509" "errors" "fmt" ios "github.com/danielpaulus/go-ios/ios" goiosmc "github.com/danielpaulus/go-ios/ios/mcinstall" "github.com/google/uuid" ) const serviceName = "com.apple.mobile.MCInstall" // Client kapselt eine offene MCInstall-Session. Muss mit Close() beendet werden. type Client struct { deviceConn ios.DeviceConnectionInterface codec ios.PlistCodec } // Open startet eine MCInstall-Session zum Gerät. func Open(device ios.DeviceEntry) (*Client, error) { conn, err := ios.ConnectToService(device, serviceName) if err != nil { return nil, fmt.Errorf("mcinstall: connect service: %w", err) } return &Client{ deviceConn: conn, codec: ios.NewPlistCodec(), }, nil } func (c *Client) Close() { if c.deviceConn != nil { c.deviceConn.Close() c.deviceConn = nil } } // sendAndReceive ist das Workhorse: encode plist, send, read response, decode. func (c *Client) sendAndReceive(req map[string]interface{}) (map[string]interface{}, error) { encoded, err := c.codec.Encode(req) if err != nil { return nil, fmt.Errorf("mcinstall: encode request: %w", err) } if err := c.deviceConn.Send(encoded); err != nil { return nil, fmt.Errorf("mcinstall: send: %w", err) } respBytes, err := c.codec.Decode(c.deviceConn.Reader()) if err != nil { return nil, fmt.Errorf("mcinstall: decode response: %w", err) } resp, err := ios.ParsePlist(respBytes) if err != nil { return nil, fmt.Errorf("mcinstall: parse plist: %w", err) } if status, ok := resp["Status"].(string); ok && status != "Acknowledged" { // Manche Commands returnen "Status":"Acknowledged", andere returnen Daten direkt. // Wir flaggen nur explizite Fehler. if errVal, ok := resp["ErrorChain"]; ok { return resp, fmt.Errorf("mcinstall: ErrorChain: %v", errVal) } } return resp, nil } // Flush — leert den MCInstall-internal cache. Standard-Vorbereitung. func (c *Client) Flush() error { _, err := c.sendAndReceive(map[string]interface{}{"RequestType": "Flush"}) return err } // HelloHostIdentifier — Apple's Handshake-Step zwischen Commands. Manche // Commands brauchen ihn als Preamble. func (c *Client) HelloHostIdentifier() error { _, err := c.sendAndReceive(map[string]interface{}{"RequestType": "HelloHostIdentifier"}) return err } // GetCloudConfiguration liest die aktuelle Cloud-Config aus dem Gerät. // Returnt nil ohne Error wenn keine Cloud-Config gesetzt ist (statt error). func (c *Client) GetCloudConfiguration() (map[string]interface{}, error) { resp, err := c.sendAndReceive(map[string]interface{}{"RequestType": "GetCloudConfiguration"}) if err != nil { return nil, err } if cfg, ok := resp["CloudConfiguration"].(map[string]interface{}); ok { return cfg, nil } return nil, nil } // SetCloudConfiguration schreibt die Cloud-Config — DAS ist der Supervise-Hebel. // Required keys: // - IsSupervised: bool // - OrganizationName: string (erscheint als "Verwaltet von X" in Settings) // - OrganizationMagic: string (UUID, einmalig pro Supervision-Session) // - SupervisorHostCertificates: [][]byte (DER-encoded Cert) // - AllowPairing: bool (true = User kann andere Macs für Sync pairen) // - IsMultiUser: bool (false für single-user iPhone, true für Shared-iPad) func (c *Client) SetCloudConfiguration(cfg map[string]interface{}) error { req := map[string]interface{}{ "RequestType": "SetCloudConfiguration", "CloudConfiguration": cfg, } _, err := c.sendAndReceive(req) return err } // Escalate authentifiziert uns gegenüber MCInstall als (any-)Supervisor via // PKCS#7-Challenge-Response. NÖTIG vor SetCloudConfiguration auf already- // supervised devices — sonst kommt ErrorCode 14002. // // Apple verifiziert NICHT dass unser Cert dem aktuellen Supervisor matched — // nur dass wir den Private-Key zu dem Cert besitzen den wir senden. Das ist // genau wie TechLockdown re-supervise kann ohne den Original-Key zu haben. func (c *Client) Escalate(cert *x509.Certificate, privateKey interface{}) error { // Step 1: unser Cert senden resp, err := c.sendAndReceive(map[string]interface{}{ "RequestType": "Escalate", "SupervisorCertificate": cert.Raw, }) if err != nil { return fmt.Errorf("escalate: %w", err) } challenge, ok := resp["Challenge"].([]byte) if !ok { return fmt.Errorf("escalate: missing Challenge in response: %v", resp) } // Step 2: Challenge signieren mit unserem Private-Key (PKCS#7-SignedData) signed, err := ios.Sign(challenge, cert, privateKey) if err != nil { return fmt.Errorf("escalate sign: %w", err) } // Step 3: signed Response zurück _, err = c.sendAndReceive(map[string]interface{}{ "RequestType": "EscalateResponse", "SignedRequest": signed, }) if err != nil { return fmt.Errorf("escalate response: %w", err) } // Step 4: keybag migration triggern _, err = c.sendAndReceive(map[string]interface{}{ "RequestType": "ProceedWithKeybagMigration", }) if err != nil { return fmt.Errorf("escalate keybag: %w", err) } return nil } // EscalateUnsupervised — go-ios's eigene Implementation wrappen. // Wirft typischerweise einen "CertificateRejected"-Error, aber funktioniert // trotzdem (siehe go-ios prepare.go-Comment). Wir loggen aber ignorieren. func EscalateUnsupervised(device ios.DeviceEntry) error { conn, err := goiosmc.New(device) if err != nil { return fmt.Errorf("mcinstall: escalate connect: %w", err) } defer conn.Close() if err := conn.EscalateUnsupervised(); err != nil { // go-ios's comment: "the device always throws a CertificateRejected // error here, but it works just fine" return err } return nil } // SuperviseConfig bündelt die Parameter für einen Supervise-Run. type SuperviseConfig struct { OrganizationName string CertDER []byte // DER-encoded Supervision-Cert AllowPairing bool // Default true — kein Reason User-Pairing zu blocken } // Supervise führt den ganzen Supervise-Flow aus auf einer geöffneten Connection. // 1. Flush // 2. HelloHostIdentifier // 3. SetCloudConfiguration(IsSupervised=true, OrgName, OrgMagic-UUID, CertBytes) // 4. (caller ruft EscalateUnsupervised separately falls nötig) // 5. GetCloudConfiguration als Verify func (c *Client) Supervise(cfg SuperviseConfig) (map[string]interface{}, error) { if cfg.OrganizationName == "" { return nil, errors.New("mcinstall: OrganizationName required") } if len(cfg.CertDER) == 0 { return nil, errors.New("mcinstall: CertDER required — generate via cert.LoadOrCreate()") } if err := c.Flush(); err != nil { return nil, fmt.Errorf("supervise: flush: %w", err) } if err := c.HelloHostIdentifier(); err != nil { return nil, fmt.Errorf("supervise: hello: %w", err) } cloudConfig := map[string]interface{}{ "IsSupervised": true, "OrganizationName": cfg.OrganizationName, "OrganizationMagic": uuid.New().String(), "SupervisorHostCertificates": [][]byte{cfg.CertDER}, "IsMultiUser": false, "AllowPairing": cfg.AllowPairing, } if err := c.SetCloudConfiguration(cloudConfig); err != nil { return nil, fmt.Errorf("supervise: SetCloudConfiguration: %w", err) } // Verify if err := c.HelloHostIdentifier(); err != nil { return nil, fmt.Errorf("supervise: hello after set: %w", err) } resp, err := c.GetCloudConfiguration() if err != nil { return nil, fmt.Errorf("supervise: verify GetCloudConfiguration: %w", err) } return resp, nil } // Unsupervise führt den Reverse-Flow aus — Cloud-Config mit IsSupervised=false zurückschreiben. func (c *Client) Unsupervise(orgName string) error { if err := c.Flush(); err != nil { return err } if err := c.HelloHostIdentifier(); err != nil { return err } cloudConfig := map[string]interface{}{ "IsSupervised": false, "OrganizationName": orgName, "IsMultiUser": false, "AllowPairing": true, } return c.SetCloudConfiguration(cloudConfig) }