Skip to main content

ios_core/lockdown/
session.rs

1use std::io::Cursor;
2use std::sync::Arc;
3
4use crate::proto::tls::InsecureSkipVerify;
5use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
6use rustls::ClientConfig;
7use tokio::io::{AsyncRead, AsyncWrite, BufReader, BufWriter};
8use tokio_rustls::client::TlsStream;
9
10use crate::lockdown::pair_record::PairRecord;
11use crate::lockdown::protocol::*;
12use crate::lockdown::LockdownError;
13
14pub const CORE_DEVICE_PROXY: &str = "com.apple.internal.devicecompute.CoreDeviceProxy";
15
16/// Perform lockdown QueryType + StartSession, then upgrade the stream to TLS via native-tls.
17///
18/// Returns (session_id, tls_reader, tls_writer).
19pub async fn start_lockdown_session<S>(
20    stream: S,
21    pair_record: &PairRecord,
22) -> Result<
23    (
24        String,
25        BufReader<tokio::io::ReadHalf<TlsStream<S>>>,
26        BufWriter<tokio::io::WriteHalf<TlsStream<S>>>,
27    ),
28    LockdownError,
29>
30where
31    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
32{
33    let (reader_raw, writer_raw) = tokio::io::split(stream);
34    let mut reader = BufReader::new(reader_raw);
35    let mut writer = BufWriter::new(writer_raw);
36
37    // 1. QueryType — confirm lockdown
38    send_lockdown(
39        &mut writer,
40        &QueryTypeRequest {
41            label: "ios-rs",
42            request: "QueryType",
43        },
44    )
45    .await?;
46    let _: QueryTypeResponse = recv_lockdown(&mut reader).await?;
47
48    // 2. StartSession
49    send_lockdown(
50        &mut writer,
51        &StartSessionRequest {
52            label: "ios-rs",
53            protocol_version: "2",
54            request: "StartSession",
55            host_id: pair_record.host_id.clone(),
56            system_buid: pair_record.system_buid.clone(),
57        },
58    )
59    .await?;
60    let session_resp: StartSessionResponse = recv_lockdown(&mut reader).await?;
61
62    if !session_resp.enable_session_ssl {
63        return Err(LockdownError::Protocol("device did not enable SSL".into()));
64    }
65
66    // 3. Upgrade to TLS
67    let stream = reader.into_inner().unsplit(writer.into_inner());
68    let tls_stream = build_rustls_connection(stream, pair_record, "lockdown").await?;
69
70    let (tls_r, tls_w) = tokio::io::split(tls_stream);
71    Ok((
72        session_resp.session_id,
73        BufReader::new(tls_r),
74        BufWriter::new(tls_w),
75    ))
76}
77
78fn build_rustls_config(pair_record: &PairRecord) -> Result<Arc<ClientConfig>, LockdownError> {
79    let _ = rustls::crypto::ring::default_provider().install_default();
80
81    let mut cert_reader = Cursor::new(&pair_record.host_certificate);
82    let cert_chain: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_reader)
83        .collect::<Result<Vec<_>, _>>()
84        .map_err(|e| {
85            LockdownError::Protocol(format!("failed to parse host certificate PEM: {e}"))
86        })?;
87    if cert_chain.is_empty() {
88        return Err(LockdownError::Protocol(
89            "pair record host certificate chain is empty".into(),
90        ));
91    }
92
93    let mut key_reader = Cursor::new(&pair_record.host_private_key);
94    let private_key: PrivateKeyDer<'static> = rustls_pemfile::private_key(&mut key_reader)
95        .map_err(|e| LockdownError::Protocol(format!("failed to parse host private key PEM: {e}")))?
96        .ok_or_else(|| LockdownError::Protocol("pair record host private key is missing".into()))?;
97
98    let config = ClientConfig::builder()
99        .dangerous()
100        .with_custom_certificate_verifier(Arc::new(InsecureSkipVerify))
101        .with_client_auth_cert(cert_chain, private_key)
102        .map_err(|e| LockdownError::Protocol(format!("rustls client auth config: {e}")))?;
103
104    Ok(Arc::new(config))
105}
106
107async fn build_rustls_connection<S>(
108    stream: S,
109    pair_record: &PairRecord,
110    server_name: &'static str,
111) -> Result<TlsStream<S>, LockdownError>
112where
113    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
114{
115    let config = build_rustls_config(pair_record)?;
116    let connector = tokio_rustls::TlsConnector::from(config);
117
118    // Hostname is ignored by our verifier, but rustls still requires a syntactically valid name.
119    let server_name = ServerName::try_from(server_name).map_err(|e| {
120        LockdownError::Protocol(format!("invalid rustls server name '{server_name}': {e}"))
121    })?;
122
123    connector
124        .connect(server_name, stream)
125        .await
126        .map_err(|e| LockdownError::Protocol(format!("TLS handshake: {e}")))
127}
128
129/// Send StartService over an established TLS session.
130pub async fn start_service<R, W>(
131    reader: &mut R,
132    writer: &mut W,
133    service_name: &str,
134) -> Result<(u16, bool), LockdownError>
135where
136    R: AsyncRead + Unpin,
137    W: AsyncWrite + Unpin,
138{
139    send_lockdown(
140        writer,
141        &StartServiceRequest {
142            label: "ios-rs",
143            request: "StartService",
144            service: service_name.to_string(),
145        },
146    )
147    .await?;
148    let resp: StartServiceResponse = recv_lockdown(reader).await?;
149
150    // Check if device returned an error instead of a port
151    if let Some(err) = resp.error {
152        return Err(LockdownError::Protocol(format!(
153            "StartService '{service_name}' failed: {err}"
154        )));
155    }
156
157    let port = resp.port.ok_or_else(|| {
158        LockdownError::Protocol(format!(
159            "StartService '{service_name}': response missing Port field"
160        ))
161    })?;
162
163    let ssl = resp.enable_service_ssl.unwrap_or(false);
164    tracing::debug!("StartService '{service_name}': port={port} enable_ssl={ssl}");
165    Ok((port, ssl))
166}
167
168/// Wrap a service stream with TLS (for services with EnableServiceSSL=true).
169pub async fn wrap_service_tls<S>(
170    stream: S,
171    pair_record: &PairRecord,
172) -> Result<TlsStream<S>, LockdownError>
173where
174    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
175{
176    wrap_service_tls_with_server_name(stream, pair_record, "lockdown").await
177}
178
179/// Wrap a service stream with TLS using an explicit rustls server name.
180///
181/// The underlying verifier still skips certificate validation, but the caller can
182/// preserve a service-specific ClientHello shape when compatibility requires it.
183pub async fn wrap_service_tls_with_server_name<S>(
184    stream: S,
185    pair_record: &PairRecord,
186    server_name: &'static str,
187) -> Result<TlsStream<S>, LockdownError>
188where
189    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
190{
191    build_rustls_connection(stream, pair_record, server_name).await
192}
193
194/// Drop the post-lockdown service TLS layer and recover the underlying stream.
195///
196/// Some legacy developer services perform a TLS handshake as a transport gate,
197/// but expect raw DTX bytes after the handshake completes.
198pub fn strip_service_tls<S>(stream: TlsStream<S>) -> Result<S, LockdownError>
199where
200    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
201{
202    let (stream, _session) = stream.into_inner();
203    Ok(stream)
204}
205
206/// Perform a TLS handshake and immediately return the underlying plaintext stream.
207///
208/// Some lockdown services require TLS only as a transport gate and expect raw bytes
209/// again once the handshake finishes.
210pub async fn handshake_only_service_tls<S>(
211    stream: S,
212    pair_record: &PairRecord,
213    server_name: &'static str,
214) -> Result<S, LockdownError>
215where
216    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
217{
218    let tls_stream = wrap_service_tls_with_server_name(stream, pair_record, server_name).await?;
219    strip_service_tls(tls_stream)
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    fn dummy_pair_record(cert: &[u8], key: &[u8]) -> PairRecord {
227        PairRecord {
228            device_certificate: b"ignored".to_vec(),
229            host_certificate: cert.to_vec(),
230            host_private_key: key.to_vec(),
231            root_certificate: b"ignored".to_vec(),
232            host_id: "test-host-id".into(),
233            system_buid: "test-buid".into(),
234            wifi_mac_address: None,
235        }
236    }
237
238    // A self-signed RSA 2048 cert+key for testing (generated offline, no secrets)
239    const TEST_CERT_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----
240MIIC/zCCAeegAwIBAgIUIcm1BZEI6nF3fXhXJuEND4sUAfcwDQYJKoZIhvcNAQEL
241BQAwDzENMAsGA1UEAwwEdGVzdDAeFw0yNjA0MzAwNDM5MzBaFw0yNzA0MzAwNDM5
242MzBaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
243AoIBAQC79dsRr759iA4cborijwjiNsZzHQvL9J5PGJlShqIC2aL9UMKCU1EPtYTR
2445F4SDz6HDS3IeXPoEErn03hNkHopg463XbVgiFXOZwzPZbahNrKksGp+Klvc0cNb
245nClv/gmKro1Q9vp3IieR7Rm1fcUpY8AJ3dINdYJvFnHyaYLgjIYLdiFCCBoKh9Mq
246iHDCHZ/ZqgV8k22MB5tooCEv+rXQqWMhhtc+L9ba/P6HvLK7F1FmJqsW2GYgFRd+
247YDgwsGB/l3Cpen2BD64iYMsKIacfl6phDNQGjmYbCsLAYD6c3csKSOsT1jZONsvN
248nNo2X/87haoD+iDj42/LnVbOMvnrAgMBAAGjUzBRMB0GA1UdDgQWBBQM7F7f8Qlu
2492FiOEX3CqqsZFql/NzAfBgNVHSMEGDAWgBQM7F7f8Qlu2FiOEX3CqqsZFql/NzAP
250BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB4iqOezp/yllYfn8JW
251eZwa+LcDyAcdo7AkBkJ2RVygyQeHwnjOcnxGJ/X6+C2/iNTtkWu4KKdi1rzMX6W7
252BMGvz0c6Jfe9c3vifv+9GmvYETZyPxbNxwA33vTOLKtq0Wcb8zPvOq05rgeXOoVU
2533IP79ijQfdaIe5MzuKUay4DFB05qgIBkIyCxPx/p2nH2jyDaMum8KyFFpyMJwdNz
2545UI1gg2kDVt669mAY5PdProZg6GHpt1Q4gDkWj1jTTNncPPyxOcnP/HQoXZtuPZu
25561OsIR2URv16qBoNfdhIS3UJ4eYt65mnXOQg1Bagotzvq3sy3B5MOJyXUMjhtuAG
256f/68
257-----END CERTIFICATE-----";
258
259    const TEST_KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY-----
260MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC79dsRr759iA4c
261borijwjiNsZzHQvL9J5PGJlShqIC2aL9UMKCU1EPtYTR5F4SDz6HDS3IeXPoEErn
26203hNkHopg463XbVgiFXOZwzPZbahNrKksGp+Klvc0cNbnClv/gmKro1Q9vp3IieR
2637Rm1fcUpY8AJ3dINdYJvFnHyaYLgjIYLdiFCCBoKh9MqiHDCHZ/ZqgV8k22MB5to
264oCEv+rXQqWMhhtc+L9ba/P6HvLK7F1FmJqsW2GYgFRd+YDgwsGB/l3Cpen2BD64i
265YMsKIacfl6phDNQGjmYbCsLAYD6c3csKSOsT1jZONsvNnNo2X/87haoD+iDj42/L
266nVbOMvnrAgMBAAECggEADm3ONmpeXjaelrIpuUCvtuXrkBSvviV2La4+vuYU89EP
267QRD9DZIly+XsX0x/qDVBYI6zcAtayXrOtUM3ngS0TBGMWCk6bkGpDKI+ioFNZszT
268I+9jDXJlAOudap/vUmiXBO1nbcq36YNWtE4WRid0hjvhFyDPKjdWHv8DGk/dOy2M
269vmnXWfcuNCdSyHAqegi/PTprRF9J9xJ+eIyTTQFeRWDVC35aE5QNhAXQfmBME/oX
2707GNjCyqITT73OTJAXLmFje65FFw/Cb84EoLrPR944K29+kRp+RNnkBRxXbqZt1kT
271zkeRO5fhq6pV0Gp66dAQB8a1BDsnQZVIsUYmApSASQKBgQDil8RXctwOW0CQT3/h
2725Qoo6DkPZLdgnc/x9JwzZ5i/sJBsruR32rM6ejV4lMn1xlyDHb5QPI5ou8Kw+Toa
273mLZdW3gNI+J+XEaskf4B1hn8RmlvAk1DAP1MJotWap2BxOa7iYDb3eE/eJSL2vzD
274s+2eBlEXzuifVjtj4h/2DSoCjwKBgQDUWpQidb2rI8bvApLDxfaMXeS/z18pqXL3
275M9j9bPXEUoTQ+u8GzV7wvaB38jIQ884YmcGVmJ5iJ41aC4/GOeWk7kXOUv95uY70
276wTxHchKojQacQPYALfMsmuPLrxCRfh0QP4dO6PZ6MMSFhQFPvV9OVn9Htyvj7TCo
277TXxljQ5Q5QKBgDRokdsAD/GqHXbDTHq89OqdO4VZ8CgCmDQINZCWJ3g+qEja8rDd
278/pJJ7dAj6cpUxNT2riv0taN3ugIgwtWf+J4DJ/MyF5LOWPJVGgDmuj/lMUGhsKkM
279s4lHaPbl1eRL3GoH1awE17JMe18VmVzSYuUn5N2y147y7O2fQXExfkP1AoGAKbRs
280WWQ0TtMk87XWqxpK9IBQN5d7ggwkZwZIvGTU06y9JunRXc2hsrgbNtNbH9cyB8TS
281rxWdLXvFGAUjRHQEdOLS1NWaFQbrW4hD1WhC39Vqke90IM7lbkIxMMR+BYT2IkXH
282xiicl5zSS8K2Yjm36QO11ZjUxtvDbZpiLvOH9z0CgYEA0Re52xUUrBkf82MYOnMG
283GA3kNp7expYQItyuLI1SgB7sOvdNR5e7y8SOTh43zEG2PHhbaB4SuOK+oX76Q49Q
284nKakrqpv4o+Dp+AJGZlvbgsADdf4WgP8C7GgLYAvEUknoqS9TrCcDYXvEG1DgmcM
285h6sEdO4FSmZFwEQ9W7FVspA=
286-----END PRIVATE KEY-----";
287
288    #[test]
289    fn build_rustls_config_succeeds_with_valid_pem() {
290        let record = dummy_pair_record(TEST_CERT_PEM, TEST_KEY_PEM);
291        let result = build_rustls_config(&record);
292        assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
293    }
294
295    #[test]
296    fn build_rustls_config_rejects_empty_certificate() {
297        let record = dummy_pair_record(b"", TEST_KEY_PEM);
298        let err = build_rustls_config(&record).unwrap_err();
299        assert!(
300            err.to_string().contains("empty"),
301            "expected 'empty' error, got: {err}"
302        );
303    }
304
305    #[test]
306    fn build_rustls_config_rejects_invalid_key() {
307        let record = dummy_pair_record(TEST_CERT_PEM, b"not a PEM key");
308        let err = build_rustls_config(&record).unwrap_err();
309        assert!(
310            err.to_string().contains("private key"),
311            "expected private key error, got: {err}"
312        );
313    }
314
315    #[test]
316    fn strip_service_tls_is_identity_for_type() {
317        // strip_service_tls just unwraps the inner stream — test type-level correctness
318        // by verifying it compiles and the function signature is correct.
319        // Actual TLS stream testing requires a real handshake (covered by integration tests).
320    }
321}