chahinebrini 01374c426e feat(supervise-magic): TechLockdown-clone v1 — supervise iPhones without erase
Implementiert eigenen MobileBackup2-Restore-Trick zur Supervision-Übernahme
von aktivierten iOS-Geräten ohne Factory-Reset. Foundation für DiGA-Phase-G
Lock-Layer-Stack (non-removable Apps, non-removable Profiles, OnDemand-VPN-
Toggle-Lock) auf Consumer-iPhones.

Verifiziert end-to-end auf:
- iPhone Air (iPhone18,4, iOS 26.5): TL→ReBreak re-supervise 
- Olfa iPhone 14 Pro (iPhone15,3, iOS 26.4.2): TL→ReBreak re-supervise 

Key empirische Findings:
1. Find-My-iPhone MUSS off sein (ErrorCode 211 sonst)
   → Stolen Device Protection (SDP) zwingt FMI an seit iOS 17.3+
2. SupervisorHostCertificates DARF NICHT in CloudConfigurationDetails sein
   für fresh-supervise auf activated unsupervised devices (sonst partial-apply)
3. MCInstall.SetCloudConfiguration firet 14002 auf allen activated devices
   → MobileBackup2-Restore-Trick ist der einzige Weg
4. TL's extracted-embed-bytes != TL's wire-output (Runtime-Mutation)
   → Verbatim-Kopieren reicht nicht

Reverse-Engineering basiert komplett auf:
- Apple's public protocol docs (devicemanagement, mobilebackup2 schemas)
- libimobiledevice (open-source reference impl)
- TL public-distributed binary (interop-RE, legal per US-DMCA-1201 + EU-2009/24)

Structure:
  cmd/supervise/         — main CLI (check, cloud-config, supervise, cert-info, unsupervise)
  cmd/dump-artifacts/    — diagnostic helper (no device needed)
  cmd/usbmux-proxy/      — MITM-proxy for TL-traffic-capture (debug)
  cmd/tl-patcher/        — patches TL's hard-coded usbmuxd path (debug)
  internal/dlmessage/    — DLMessage wire-protocol (4-byte BE length + plist)
  internal/mobilebackup2/— mobilebackup2-service impl (BaseVersionExchange,
                          Hello, Restore, ServeFiles + TL-extracted templates)
  internal/cloudconfig/  — CloudConfigurationDetails.plist builder (cert-less,
                          25 keys matching TL's runtime-output)
  internal/cert/         — auto-gen + persist supervisor-cert in ~/.rebreak-supervise/
  internal/mcinstall/    — MCInstall.GetCloudConfiguration für state-checks
  internal/device/       — go-ios DeviceEntry wrapper
  internal/afclock/      — AFC sync-lock auf /com.apple.itunes.lock_sync
  internal/notification_proxy/ — PostNotification (syncWillStart/etc)
  internal/preflight/    — FMI/Activation/OS-version pre-checks
  internal/supervise/    — End-to-end Flow-Orchestrierung (MobileBackup2 default,
                          MCInstall via REBREAK_FORCE_MCINSTALL=1)

Pending für volle Productization (Phase G):
- Fresh-supervise direkt-empirisch auf truly-unsupervised iPhone testen
  (heute Nacht nur durch Inferenz aus TL-Verhalten gestützt)
- Auto-MDM-enroll-Step nach Supervise (ConfigurationURL oder cfgutil-style)
- DiGA-Onboarding-Flow + Lyra-Coach für FMI/SDP-Disable
- Multi-Device-Validation (Modelle, iOS-Versionen)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 01:55:10 +02:00

132 lines
3.6 KiB
Go

// Package cert managed die Supervision-Identity (Cert + Private-Key).
// Cert wird einmal generiert via go-ios und persistent unter
// ~/.rebreak-supervise/ abgelegt. Default-Path matched die existing
// Bootstrap-Tool-Konvention.
package cert
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
"path/filepath"
ios "github.com/danielpaulus/go-ios/ios"
)
// Identity ist unsere komplette Supervision-Identity.
// CertDER ist das was wir SetCloudConfiguration als SupervisorHostCertificates übergeben.
type Identity struct {
CertDER []byte
PrivateKeyDER []byte
}
// DefaultDir matches die Konvention aus dem alten bootstrap-tool.
func DefaultDir() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".rebreak-supervise")
}
// LoadOrCreate lädt eine existierende Identity aus dem default-dir, oder
// generiert eine neue + speichert sie. Idempotent: zweiter Call returnt
// die gleiche Identity wie der erste.
func LoadOrCreate() (*Identity, error) {
dir := DefaultDir()
certPath := filepath.Join(dir, "supervision-cert.pem")
keyPath := filepath.Join(dir, "supervision-key.pem")
id, err := load(certPath, keyPath)
if err == nil {
return id, nil
}
if !os.IsNotExist(err) && !errors.Is(err, errCorruptIdentity) {
return nil, fmt.Errorf("cert: load existing: %w", err)
}
// Generieren
id, err = generate()
if err != nil {
return nil, fmt.Errorf("cert: generate: %w", err)
}
if err := id.save(certPath, keyPath); err != nil {
return nil, fmt.Errorf("cert: save: %w", err)
}
return id, nil
}
// load aus PEM-Files.
func load(certPath, keyPath string) (*Identity, error) {
certPEM, err := os.ReadFile(certPath)
if err != nil {
return nil, err
}
keyPEM, err := os.ReadFile(keyPath)
if err != nil {
return nil, err
}
certBlock, _ := pem.Decode(certPEM)
keyBlock, _ := pem.Decode(keyPEM)
if certBlock == nil || keyBlock == nil {
return nil, errCorruptIdentity
}
return &Identity{
CertDER: certBlock.Bytes,
PrivateKeyDER: keyBlock.Bytes,
}, nil
}
// generate via go-ios's CreateDERFormattedSupervisionCert.
func generate() (*Identity, error) {
ca, err := ios.CreateDERFormattedSupervisionCert()
if err != nil {
return nil, err
}
return &Identity{
CertDER: ca.CertDER,
PrivateKeyDER: ca.PrivateKeyDER,
}, nil
}
func (id *Identity) save(certPath, keyPath string) error {
dir := filepath.Dir(certPath)
if err := os.MkdirAll(dir, 0o700); err != nil {
return err
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: id.CertDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: id.PrivateKeyDER})
if err := os.WriteFile(certPath, certPEM, 0o600); err != nil {
return err
}
if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
return err
}
return nil
}
var errCorruptIdentity = errors.New("cert: identity files corrupt — delete + regenerate")
// Parse decodes the DER-encoded cert + private key for use with Escalate.
func (id *Identity) Parse() (*x509.Certificate, *rsa.PrivateKey, error) {
cert, err := x509.ParseCertificate(id.CertDER)
if err != nil {
return nil, nil, fmt.Errorf("cert: parse cert DER: %w", err)
}
// PrivateKey ist von go-ios als PKCS#1 DER encoded
key, err := x509.ParsePKCS1PrivateKey(id.PrivateKeyDER)
if err != nil {
// Fallback: PKCS#8
k8, err2 := x509.ParsePKCS8PrivateKey(id.PrivateKeyDER)
if err2 != nil {
return nil, nil, fmt.Errorf("cert: parse key DER (PKCS1 + PKCS8 failed): %w / %v", err, err2)
}
rsaKey, ok := k8.(*rsa.PrivateKey)
if !ok {
return nil, nil, fmt.Errorf("cert: key is not RSA")
}
key = rsaKey
}
return cert, key, nil
}