Implement pairing

This commit is contained in:
Jackson Coxson
2025-04-29 15:32:06 -06:00
parent 728711813e
commit 5da305bbe7
4 changed files with 214 additions and 1 deletions

View File

@@ -40,7 +40,13 @@ reqwest = { version = "0.12", features = [
rand = { version = "0.9", optional = true }
futures = { version = "0.3", optional = true }
sha2 = { version = "0.10", optional = true }
sha2 = { version = "0.10", optional = true, features = ["oid"] }
rsa = { version = "0.9", optional = true, features = ["sha2"] }
x509-cert = { version = "0.2", optional = true, features = [
"builder",
"pem",
], default-features = false }
[dev-dependencies]
tokio = { version = "1.43", features = ["fs"] }
@@ -59,6 +65,7 @@ springboardservices = []
misagent = []
mobile_image_mounter = ["dep:sha2"]
location_simulation = []
pair = ["chrono/default", "dep:sha2", "dep:rsa", "dep:x509-cert"]
tcp = ["tokio/net"]
tunnel_tcp_stack = ["dep:rand", "dep:futures", "tokio/fs", "tokio/sync"]
tss = ["dep:uuid", "dep:reqwest"]
@@ -82,6 +89,7 @@ full = [
"installation_proxy",
"misagent",
"mobile_image_mounter",
"pair",
"usbmuxd",
"xpc",
"location_simulation",

103
idevice/src/ca.rs Normal file
View File

@@ -0,0 +1,103 @@
// Jackson Coxson
// Inspired by pymobiledevice3
use std::str::FromStr;
use rsa::{
pkcs1::DecodeRsaPublicKey,
pkcs1v15::SigningKey,
pkcs8::{EncodePrivateKey, LineEnding, SubjectPublicKeyInfo},
RsaPrivateKey, RsaPublicKey,
};
use sha2::Sha256;
use x509_cert::{
builder::{Builder, CertificateBuilder, Profile},
der::EncodePem,
name::Name,
serial_number::SerialNumber,
time::Validity,
Certificate,
};
#[derive(Clone, Debug)]
pub struct CaReturn {
pub host_cert: Vec<u8>,
pub dev_cert: Vec<u8>,
pub private_key: Vec<u8>,
}
pub fn make_cert(
signing_key: &RsaPrivateKey,
public_key: &RsaPublicKey,
common_name: Option<&str>,
) -> Result<Certificate, Box<dyn std::error::Error>> {
// Create subject/issuer name
let name = match common_name {
Some(name) => Name::from_str(&format!("CN={name}"))?,
None => Name::default(),
};
// Set validity (10 years)
let validity = Validity::from_now(std::time::Duration::from_secs(
365 * 9 * 12 * 31 * 24 * 60 * 60, // idk like 9 years
))?;
let signing_key = SigningKey::<Sha256>::new(signing_key.clone());
let public_key = SubjectPublicKeyInfo::from_key(public_key.clone())?;
// Build certificate
let cert = CertificateBuilder::new(
Profile::Root,
SerialNumber::new(&[1])?,
validity,
name,
public_key,
&signing_key,
)?;
// Sign the certificate
let tbs_cert = cert.build()?;
Ok(tbs_cert)
}
// Equivalent to dump_cert
fn dump_cert(cert: &Certificate) -> Result<String, Box<dyn std::error::Error>> {
let b = cert.to_pem(LineEnding::LF)?;
Ok(b)
}
pub(crate) fn generate_certificates(
device_public_key_pem: &[u8],
private_key: Option<RsaPrivateKey>,
) -> Result<CaReturn, Box<dyn std::error::Error>> {
// Load device public key
println!("{}", std::str::from_utf8(device_public_key_pem)?);
let device_public_key =
RsaPublicKey::from_pkcs1_pem(std::str::from_utf8(device_public_key_pem)?)?;
// Generate or use provided private key
let private_key = match private_key {
Some(p) => p,
None => {
let mut rng = rsa::rand_core::OsRng;
RsaPrivateKey::new(&mut rng, 2048)?
}
};
// Create CA cert
let ca_public_key = RsaPublicKey::from(&private_key);
let ca_cert = make_cert(&private_key, &ca_public_key, None)?;
// Create device cert
let dev_cert = make_cert(&private_key, &device_public_key, Some("Device"))?;
Ok(CaReturn {
host_cert: dump_cert(&ca_cert)?.into_bytes(),
dev_cert: dump_cert(&dev_cert)?.into_bytes(),
private_key: private_key
.to_pkcs8_pem(LineEnding::LF)?
.as_bytes()
.to_vec(),
})
}

View File

@@ -5,6 +5,8 @@
pub mod afc;
#[cfg(feature = "amfi")]
pub mod amfi;
#[cfg(feature = "pair")]
mod ca;
#[cfg(feature = "core_device_proxy")]
pub mod core_device_proxy;
#[cfg(feature = "debug_proxy")]
@@ -413,6 +415,14 @@ pub enum IdeviceError {
#[error("image not mounted")]
ImageNotMounted,
#[cfg(feature = "pair")]
#[error("pairing trust dialog pending")]
PairingDialogResponsePending,
#[cfg(feature = "pair")]
#[error("user denied pairing trust")]
UserDeniedPairing,
#[cfg(feature = "misagent")]
#[error("misagent operation failed")]
MisagentFailure,
@@ -496,6 +506,10 @@ impl IdeviceError {
"InvalidHostID" => Some(Self::InvalidHostID),
"SessionInactive" => Some(Self::SessionInactive),
"DeviceLocked" => Some(Self::DeviceLocked),
#[cfg(feature = "pair")]
"PairingDialogResponsePending" => Some(Self::PairingDialogResponsePending),
#[cfg(feature = "pair")]
"UserDeniedPairing" => Some(Self::UserDeniedPairing),
"InternalError" => {
let detailed_error = context
.get("DetailedError")

View File

@@ -250,6 +250,94 @@ impl LockdownClient {
}
}
}
/// Generates a pairing file and sends it to the device for trusting.
/// Note that this does NOT save the file to usbmuxd's cache. That's a responsibility of the
/// caller.
/// Note that this function is computationally heavy in a debug build.
///
/// # Arguments
/// * `host_id` - The host ID, in the form of a UUID. Typically generated from the host name
/// * `wifi_mac` - The MAC address of the WiFi interface. Does not affect anything.
/// * `system_buid` - UUID fetched from usbmuxd. Doesn't appear to affect function.
///
/// # Returns
/// The newly generated pairing record
///
/// # Errors
/// Returns `IdeviceError`
#[cfg(feature = "pair")]
pub async fn pair(
&mut self,
host_id: impl Into<String>,
wifi_mac: impl Into<String>,
system_buid: impl Into<String>,
) -> Result<crate::pairing_file::PairingFile, IdeviceError> {
let host_id = host_id.into();
let wifi_mac = wifi_mac.into();
let system_buid = system_buid.into();
let pub_key = self.get_value("DevicePublicKey", None).await?;
let pub_key = match pub_key.as_data().map(|x| x.to_vec()) {
Some(p) => p,
None => {
log::warn!("Did not get public key data response");
return Err(IdeviceError::UnexpectedResponse);
}
};
let ca = crate::ca::generate_certificates(&pub_key, None).unwrap();
let mut pair_record = plist::Dictionary::new();
pair_record.insert("DevicePublicKey".into(), plist::Value::Data(pub_key));
pair_record.insert("DeviceCertificate".into(), plist::Value::Data(ca.dev_cert));
pair_record.insert(
"HostCertificate".into(),
plist::Value::Data(ca.host_cert.clone()),
);
pair_record.insert("HostID".into(), host_id.into());
pair_record.insert("RootCertificate".into(), plist::Value::Data(ca.host_cert));
pair_record.insert(
"RootPrivateKey".into(),
plist::Value::Data(ca.private_key.clone()),
);
pair_record.insert("WiFiMACAddress".into(), wifi_mac.into());
pair_record.insert("SystemBUID".into(), system_buid.into());
let mut options = plist::Dictionary::new();
options.insert("ExtendedPairingErrors".into(), true.into());
let mut req = plist::Dictionary::new();
req.insert("Label".into(), self.idevice.label.clone().into());
req.insert("Request".into(), "Pair".into());
req.insert(
"PairRecord".into(),
plist::Value::Dictionary(pair_record.clone()),
);
req.insert("ProtocolVersion".into(), "2".into());
req.insert("PairingOptions".into(), plist::Value::Dictionary(options));
loop {
self.idevice.send_plist(req.clone().into()).await?;
match self.idevice.read_plist().await {
Ok(escrow) => {
pair_record.insert("HostPrivateKey".into(), plist::Value::Data(ca.private_key));
if let Some(escrow) = escrow.get("EscrowBag").and_then(|x| x.as_data()) {
pair_record.insert("EscrowBag".into(), plist::Value::Data(escrow.to_vec()));
}
let p = crate::pairing_file::PairingFile::from_value(
&plist::Value::Dictionary(pair_record),
)?;
break Ok(p);
}
Err(IdeviceError::PairingDialogResponsePending) => {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
Err(e) => break Err(e),
}
}
}
}
impl From<Idevice> for LockdownClient {