Skip to main content

ios_core/services/prepare/
mod.rs

1//! Helpers for supervised device preparation workflows.
2//!
3//! Reference: go-ios `mcinstall/prepare.go` and `crypto_utils.go`
4
5use openssl::asn1::Asn1Time;
6use openssl::bn::{BigNum, MsbOption};
7use openssl::hash::MessageDigest;
8use openssl::nid::Nid;
9use openssl::pkcs12::Pkcs12;
10use openssl::pkey::PKey;
11use openssl::rsa::Rsa;
12use openssl::x509::extension::{BasicConstraints, ExtendedKeyUsage, KeyUsage};
13use openssl::x509::{X509NameBuilder, X509};
14use uuid::Uuid;
15
16pub const DEFAULT_ORGANIZATION_NAME: &str = "ios-rs";
17pub const DEFAULT_LANGUAGE: &str = "en";
18pub const DEFAULT_LOCALE: &str = "en_US";
19pub const DEFAULT_SKIP_SETUP_KEYS: &[&str] = &[
20    "Accessibility",
21    "AccessibilityAppearance",
22    "ActionButton",
23    "AgeAssurance",
24    "AgeBasedSafetySettings",
25    "Android",
26    "Appearance",
27    "AppleID",
28    "AppStore",
29    "Avatar",
30    "Biometric",
31    "CameraButton",
32    "CloudStorage",
33    "DeviceProtection",
34    "DeviceToDeviceMigration",
35    "Diagnostics",
36    "Display",
37    "EnableLockdownMode",
38    "ExpressLanguage",
39    "FileVault",
40    "iCloudDiagnostics",
41    "iCloudStorage",
42    "iMessageAndFaceTime",
43    "IntendedUser",
44    "Intelligence",
45    "Keyboard",
46    "Language",
47    "LanguageAndLocale",
48    "Location",
49    "LockdownMode",
50    "MessagingActivationUsingPhoneNumber",
51    "Multitasking",
52    "OSShowCase",
53    "Passcode",
54    "Payment",
55    "PreferredLanguage",
56    "Privacy",
57    "Region",
58    "Registration",
59    "Restore",
60    "RestoreCompleted",
61    "Safety",
62    "SafetyAndHandling",
63    "ScreenSaver",
64    "ScreenTime",
65    "SIMSetup",
66    "Siri",
67    "SoftwareUpdate",
68    "SpokenLanguage",
69    "TapToSetup",
70    "TermsOfAddress",
71    "Tips",
72    "Tone",
73    "TOS",
74    "TouchID",
75    "TrueToneDisplay",
76    "TVHomeScreenSync",
77    "TVProviderSignIn",
78    "TVRoom",
79    "UnlockWithWatch",
80    "UpdateCompleted",
81    "Wallpaper",
82    "WatchMigration",
83    "WebContentFiltering",
84    "Welcome",
85    "WiFi",
86    "DisplayTone",
87    "HomeButtonSensitivity",
88    "OnBoarding",
89    "Zoom",
90];
91
92#[derive(Debug, thiserror::Error)]
93pub enum PrepareError {
94    #[error("crypto error: {0}")]
95    Crypto(String),
96    #[error("plist error: {0}")]
97    Plist(#[from] plist::Error),
98}
99
100#[derive(Debug, Clone)]
101pub struct SupervisionIdentity {
102    pub certificate_der: Vec<u8>,
103    pub certificate_pem: Vec<u8>,
104    pub private_key_pem: Vec<u8>,
105    pub pkcs12_der: Vec<u8>,
106}
107
108pub fn generate_supervision_identity(
109    common_name: &str,
110    password: &str,
111) -> Result<SupervisionIdentity, PrepareError> {
112    let rsa = Rsa::generate(2048).map_err(crypto_err)?;
113    let pkey = PKey::from_rsa(rsa).map_err(crypto_err)?;
114
115    let mut name_builder = X509NameBuilder::new().map_err(crypto_err)?;
116    let subject = if common_name.trim().is_empty() {
117        DEFAULT_ORGANIZATION_NAME
118    } else {
119        common_name
120    };
121    name_builder
122        .append_entry_by_nid(Nid::COMMONNAME, subject)
123        .map_err(crypto_err)?;
124    let name = name_builder.build();
125
126    let mut serial = BigNum::new().map_err(crypto_err)?;
127    serial
128        .rand(159, MsbOption::MAYBE_ZERO, false)
129        .map_err(crypto_err)?;
130    let serial = serial.to_asn1_integer().map_err(crypto_err)?;
131
132    let mut builder = X509::builder().map_err(crypto_err)?;
133    builder.set_version(2).map_err(crypto_err)?;
134    builder.set_serial_number(&serial).map_err(crypto_err)?;
135    builder.set_subject_name(&name).map_err(crypto_err)?;
136    builder.set_issuer_name(&name).map_err(crypto_err)?;
137    builder.set_pubkey(&pkey).map_err(crypto_err)?;
138
139    let not_before = Asn1Time::days_from_now(0).map_err(crypto_err)?;
140    let not_after = Asn1Time::days_from_now(3650).map_err(crypto_err)?;
141    builder.set_not_before(&not_before).map_err(crypto_err)?;
142    builder.set_not_after(&not_after).map_err(crypto_err)?;
143    builder
144        .append_extension(
145            BasicConstraints::new()
146                .critical()
147                .ca()
148                .build()
149                .map_err(crypto_err)?,
150        )
151        .map_err(crypto_err)?;
152    builder
153        .append_extension(
154            KeyUsage::new()
155                .digital_signature()
156                .key_cert_sign()
157                .build()
158                .map_err(crypto_err)?,
159        )
160        .map_err(crypto_err)?;
161    builder
162        .append_extension(
163            ExtendedKeyUsage::new()
164                .server_auth()
165                .client_auth()
166                .build()
167                .map_err(crypto_err)?,
168        )
169        .map_err(crypto_err)?;
170    builder
171        .sign(&pkey, MessageDigest::sha512())
172        .map_err(crypto_err)?;
173
174    let cert = builder.build();
175    #[allow(deprecated)]
176    let pkcs12 = Pkcs12::builder()
177        .build(password, subject, &pkey, &cert)
178        .map_err(crypto_err)?;
179
180    Ok(SupervisionIdentity {
181        certificate_der: cert.to_der().map_err(crypto_err)?,
182        certificate_pem: cert.to_pem().map_err(crypto_err)?,
183        private_key_pem: pkey.private_key_to_pem_pkcs8().map_err(crypto_err)?,
184        pkcs12_der: pkcs12.to_der().map_err(crypto_err)?,
185    })
186}
187
188pub fn build_cloud_configuration(
189    skip_setup: &[String],
190    supervision_certificate_der: Option<&[u8]>,
191    organization_name: Option<&str>,
192) -> plist::Dictionary {
193    let mut cloud = plist::Dictionary::from_iter([
194        ("AllowPairing".to_string(), plist::Value::Boolean(true)),
195        (
196            "SkipSetup".to_string(),
197            plist::Value::Array(
198                skip_setup
199                    .iter()
200                    .cloned()
201                    .map(plist::Value::String)
202                    .collect(),
203            ),
204        ),
205    ]);
206
207    if let Some(cert) = supervision_certificate_der {
208        cloud.insert(
209            "OrganizationName".to_string(),
210            plist::Value::String(
211                organization_name
212                    .unwrap_or(DEFAULT_ORGANIZATION_NAME)
213                    .to_string(),
214            ),
215        );
216        cloud.insert(
217            "OrganizationMagic".to_string(),
218            plist::Value::String(Uuid::new_v4().to_string()),
219        );
220        cloud.insert(
221            "SupervisorHostCertificates".to_string(),
222            plist::Value::Array(vec![plist::Value::Data(cert.to_vec())]),
223        );
224        cloud.insert("IsSupervised".to_string(), plist::Value::Boolean(true));
225        cloud.insert("IsMultiUser".to_string(), plist::Value::Boolean(false));
226    }
227
228    cloud
229}
230
231pub fn build_initial_profile() -> Result<Vec<u8>, PrepareError> {
232    let payload_uuid = Uuid::new_v4().to_string();
233    let content_uuid = Uuid::new_v4().to_string();
234    let payload = plist::Value::Dictionary(plist::Dictionary::from_iter([
235        (
236            "PayloadContent".to_string(),
237            plist::Value::Array(vec![plist::Value::Dictionary(
238                plist::Dictionary::from_iter([
239                    (
240                        "PayloadDescription".to_string(),
241                        plist::Value::String("Configures Restrictions".into()),
242                    ),
243                    (
244                        "PayloadDisplayName".to_string(),
245                        plist::Value::String("Restrictions".into()),
246                    ),
247                    (
248                        "PayloadIdentifier".to_string(),
249                        plist::Value::String(format!("com.apple.applicationaccess.{content_uuid}")),
250                    ),
251                    (
252                        "PayloadType".to_string(),
253                        plist::Value::String("com.apple.applicationaccess".into()),
254                    ),
255                    (
256                        "PayloadUUID".to_string(),
257                        plist::Value::String(content_uuid),
258                    ),
259                    (
260                        "PayloadVersion".to_string(),
261                        plist::Value::Integer(1.into()),
262                    ),
263                    (
264                        "allowAppInstallation".to_string(),
265                        plist::Value::Boolean(true),
266                    ),
267                    ("allowAppRemoval".to_string(), plist::Value::Boolean(true)),
268                    ("allowCamera".to_string(), plist::Value::Boolean(true)),
269                    ("allowCloudBackup".to_string(), plist::Value::Boolean(true)),
270                    (
271                        "allowDiagnosticSubmission".to_string(),
272                        plist::Value::Boolean(true),
273                    ),
274                ]),
275            )]),
276        ),
277        (
278            "PayloadDisplayName".to_string(),
279            plist::Value::String("Device Preparation".into()),
280        ),
281        (
282            "PayloadIdentifier".to_string(),
283            plist::Value::String(format!("com.apple.prepare.{payload_uuid}")),
284        ),
285        (
286            "PayloadRemovalDisallowed".to_string(),
287            plist::Value::Boolean(false),
288        ),
289        (
290            "PayloadType".to_string(),
291            plist::Value::String("Configuration".into()),
292        ),
293        (
294            "PayloadUUID".to_string(),
295            plist::Value::String(payload_uuid),
296        ),
297        (
298            "PayloadVersion".to_string(),
299            plist::Value::Integer(1.into()),
300        ),
301    ]));
302
303    let mut buf = Vec::new();
304    plist::to_writer_xml(&mut buf, &payload)?;
305    Ok(buf)
306}
307
308fn crypto_err(err: openssl::error::ErrorStack) -> PrepareError {
309    PrepareError::Crypto(err.to_string())
310}