mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 06:26:16 +01:00
Implement cert revoke and prompt modes
This commit is contained in:
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -1793,9 +1793,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rootcause"
|
||||
version = "0.11.1"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a751633dcb95a6b1c954f0fa15c2afd9b4802640f8045432f68a1f4bde4b871"
|
||||
checksum = "03621279b1bafd0cd806d4a4e301530bfab4a54a9a572ea45a4fe5072c3e134b"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"indexmap",
|
||||
@@ -1806,9 +1806,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rootcause-internals"
|
||||
version = "0.11.1"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9eeddca0d656f1a58ce3fc3f41b0b877a7e760460108712ad39b60181fdcb3e"
|
||||
checksum = "2a6575ad7db4a6f026820de38c377b3e06fc59ceac225f868dfede39cd70e432"
|
||||
dependencies = [
|
||||
"triomphe",
|
||||
]
|
||||
|
||||
@@ -4,8 +4,11 @@ use idevice::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection};
|
||||
use isideload::{
|
||||
anisette::remote_v3::RemoteV3AnisetteProvider,
|
||||
auth::apple_account::AppleAccount,
|
||||
dev::developer_session::DeveloperSession,
|
||||
sideload::{SideloaderBuilder, TeamSelection},
|
||||
dev::{
|
||||
certificates::DevelopmentCertificate, developer_session::DeveloperSession,
|
||||
teams::DeveloperTeam,
|
||||
},
|
||||
sideload::{SideloaderBuilder, TeamSelection, builder::MaxCertsBehavior},
|
||||
};
|
||||
|
||||
use tracing::Level;
|
||||
@@ -70,25 +73,58 @@ async fn main() {
|
||||
.unwrap()
|
||||
.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "isideload-demo");
|
||||
|
||||
let team_selection_prompt = |teams: &Vec<DeveloperTeam>| {
|
||||
println!("Please select a team:");
|
||||
for (index, team) in teams.iter().enumerate() {
|
||||
println!(
|
||||
"{}: {} ({})",
|
||||
index + 1,
|
||||
team.name.as_deref().unwrap_or("<Unnamed>"),
|
||||
team.team_id
|
||||
);
|
||||
}
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
let selection = input.trim().parse::<usize>().ok()?;
|
||||
if selection == 0 || selection > teams.len() {
|
||||
return None;
|
||||
}
|
||||
Some(teams[selection - 1].team_id.clone())
|
||||
};
|
||||
|
||||
let cert_selection_prompt = |certs: &Vec<DevelopmentCertificate>| {
|
||||
println!("Maximum number of certificates reached. Please select certificates to revoke:");
|
||||
for (index, cert) in certs.iter().enumerate() {
|
||||
println!(
|
||||
"({}) {}: {}",
|
||||
index + 1,
|
||||
cert.name.as_deref().unwrap_or("<Unnamed>"),
|
||||
cert.machine_name.as_deref().unwrap_or("<No Machine Name>"),
|
||||
);
|
||||
}
|
||||
println!("Enter the numbers of the certificates to revoke, separated by commas:");
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
let selections: Vec<usize> = input
|
||||
.trim()
|
||||
.split(',')
|
||||
.filter_map(|s| s.trim().parse::<usize>().ok())
|
||||
.filter(|&n| n > 0 && n <= certs.len())
|
||||
.collect();
|
||||
if selections.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
selections
|
||||
.into_iter()
|
||||
.map(|n| certs[n - 1].clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
};
|
||||
|
||||
let mut sideloader = SideloaderBuilder::new(dev_session, apple_id.to_string())
|
||||
.team_selection(TeamSelection::Prompt(|teams| {
|
||||
println!("Please select a team:");
|
||||
for (index, team) in teams.iter().enumerate() {
|
||||
println!(
|
||||
"{}: {} ({})",
|
||||
index + 1,
|
||||
team.name.as_deref().unwrap_or("<Unnamed>"),
|
||||
team.team_id
|
||||
);
|
||||
}
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
let selection = input.trim().parse::<usize>().ok()?;
|
||||
if selection == 0 || selection > teams.len() {
|
||||
return None;
|
||||
}
|
||||
Some(teams[selection - 1].team_id.clone())
|
||||
}))
|
||||
.team_selection(TeamSelection::Prompt(team_selection_prompt))
|
||||
.max_certs_behavior(MaxCertsBehavior::Prompt(cert_selection_prompt))
|
||||
.build();
|
||||
|
||||
let result = sideloader.install_app(&provider, app_path).await;
|
||||
|
||||
@@ -27,7 +27,7 @@ rand = "0.10.0"
|
||||
uuid = {version = "1.20.0", features = ["v4"] }
|
||||
tracing = "0.1.44"
|
||||
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots"] }
|
||||
rootcause = "0.11.1"
|
||||
rootcause = "0.12.0"
|
||||
futures-util = "0.3.31"
|
||||
serde_json = "1.0.149"
|
||||
base64 = "0.22.1"
|
||||
|
||||
@@ -63,7 +63,6 @@ impl AppleAccount {
|
||||
anisette_generator: AnisetteDataGenerator,
|
||||
debug: bool,
|
||||
) -> Result<Self, Report> {
|
||||
info!("Initializing apple account");
|
||||
if debug {
|
||||
warn!("Debug mode enabled: this is a security risk!");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::{
|
||||
dev::{developer_session::DeveloperSession, teams::DeveloperTeam},
|
||||
dev::{
|
||||
certificates::DevelopmentCertificate, developer_session::DeveloperSession,
|
||||
teams::DeveloperTeam,
|
||||
},
|
||||
sideload::sideloader::Sideloader,
|
||||
util::storage::SideloadingStorage,
|
||||
};
|
||||
@@ -27,10 +30,12 @@ impl Display for TeamSelection {
|
||||
}
|
||||
|
||||
pub enum MaxCertsBehavior {
|
||||
/// If the maximum number of certificates is reached, delete all existing certificates and create a new one
|
||||
/// If the maximum number of certificates is reached, revoke certs until it is possible to create a new certificate
|
||||
Revoke,
|
||||
/// If the maximum number of certificates is reached, return an error instead of creating a new certificate
|
||||
Error,
|
||||
/// If the maximum number of certificates is reached, prompt the user to select which certificates to revoke until it is possible to create a new certificate
|
||||
Prompt(fn(&Vec<DevelopmentCertificate>) -> Option<Vec<DevelopmentCertificate>>),
|
||||
}
|
||||
|
||||
pub struct SideloaderBuilder {
|
||||
@@ -69,12 +74,17 @@ impl SideloaderBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_certs_behavior(mut self, behavior: MaxCertsBehavior) -> Self {
|
||||
self.max_certs_behavior = Some(behavior);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Sideloader {
|
||||
Sideloader::new(
|
||||
self.developer_session,
|
||||
self.apple_email,
|
||||
self.team_selection.unwrap_or(TeamSelection::First),
|
||||
self.max_certs_behavior.unwrap_or(MaxCertsBehavior::Revoke),
|
||||
self.max_certs_behavior.unwrap_or(MaxCertsBehavior::Error),
|
||||
self.machine_name.unwrap_or_else(|| "isideload".to_string()),
|
||||
self.storage
|
||||
.unwrap_or_else(|| Box::new(crate::util::storage::new_storage())),
|
||||
|
||||
@@ -11,8 +11,11 @@ use tracing::{error, info};
|
||||
use x509_certificate::X509Certificate;
|
||||
|
||||
use crate::{
|
||||
SideloadError,
|
||||
dev::{
|
||||
certificates::CertificatesApi, developer_session::DeveloperSession, teams::DeveloperTeam,
|
||||
certificates::{CertificatesApi, DevelopmentCertificate},
|
||||
developer_session::DeveloperSession,
|
||||
teams::DeveloperTeam,
|
||||
},
|
||||
sideload::builder::MaxCertsBehavior,
|
||||
util::storage::SideloadingStorage,
|
||||
@@ -22,6 +25,7 @@ pub struct CertificateIdentity {
|
||||
pub machine_id: String,
|
||||
pub machine_name: String,
|
||||
pub certificate: X509Certificate,
|
||||
pub private_key: RsaPrivateKey,
|
||||
}
|
||||
|
||||
impl CertificateIdentity {
|
||||
@@ -36,23 +40,37 @@ impl CertificateIdentity {
|
||||
let pr = Self::retrieve_private_key(apple_email, storage).await?;
|
||||
|
||||
let found = Self::find_matching(&pr, machine_name, developer_session, team).await;
|
||||
if let Ok(Some(cert)) = found {
|
||||
if let Ok(Some((cert, x509_cert))) = found {
|
||||
info!("Found matching certificate");
|
||||
return Ok(cert);
|
||||
return Ok(Self {
|
||||
machine_id: cert.machine_id.clone().unwrap_or_default(),
|
||||
machine_name: cert.machine_name.clone().unwrap_or_default(),
|
||||
certificate: x509_cert,
|
||||
private_key: pr,
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(e) = found {
|
||||
error!("Failed to check for matching certificate: {:?}", e);
|
||||
}
|
||||
info!("Requesting new certificate");
|
||||
Self::request_certificate(
|
||||
let (cert, x509_cert) = Self::request_certificate(
|
||||
&pr,
|
||||
machine_name.to_string(),
|
||||
developer_session,
|
||||
team,
|
||||
max_certs_behavior,
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
info!("Successfully obtained certificate");
|
||||
|
||||
Ok(Self {
|
||||
machine_id: cert.machine_id.clone().unwrap_or_default(),
|
||||
machine_name: cert.machine_name.clone().unwrap_or_default(),
|
||||
certificate: x509_cert,
|
||||
private_key: pr,
|
||||
})
|
||||
}
|
||||
|
||||
async fn retrieve_private_key(
|
||||
@@ -63,16 +81,16 @@ impl CertificateIdentity {
|
||||
hasher.update(apple_email.as_bytes());
|
||||
let email_hash = hex::encode(hasher.finalize());
|
||||
|
||||
let private_key = storage.retrieve(&format!("{}/key", email_hash))?;
|
||||
let private_key = storage.retrieve_data(&format!("{}/key", email_hash))?;
|
||||
if private_key.is_some() {
|
||||
return Ok(RsaPrivateKey::from_pkcs8_pem(&private_key.unwrap())?);
|
||||
return Ok(RsaPrivateKey::from_pkcs8_der(&private_key.unwrap())?);
|
||||
}
|
||||
|
||||
let mut rng = rand::rng();
|
||||
let private_key = RsaPrivateKey::new(&mut rng, 2048)?;
|
||||
storage.store(
|
||||
storage.store_data(
|
||||
&format!("{}/key", email_hash),
|
||||
&private_key.to_pkcs8_pem(Default::default())?.to_string(),
|
||||
&private_key.to_pkcs8_der()?.as_bytes(),
|
||||
)?;
|
||||
|
||||
Ok(private_key)
|
||||
@@ -83,7 +101,7 @@ impl CertificateIdentity {
|
||||
machine_name: &str,
|
||||
developer_session: &mut DeveloperSession,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<Option<Self>, Report> {
|
||||
) -> Result<Option<(DevelopmentCertificate, X509Certificate)>, Report> {
|
||||
let public_key_der = private_key
|
||||
.to_public_key()
|
||||
.to_pkcs1_der()?
|
||||
@@ -103,11 +121,7 @@ impl CertificateIdentity {
|
||||
X509Certificate::from_der(cert.cert_content.as_ref().unwrap().as_ref())?;
|
||||
|
||||
if public_key_der == x509_cert.public_key_data().as_ref() {
|
||||
return Ok(Some(Self {
|
||||
machine_id: cert.machine_id.clone().unwrap_or_default(),
|
||||
machine_name: cert.machine_name.clone().unwrap_or_default(),
|
||||
certificate: x509_cert,
|
||||
}));
|
||||
return Ok(Some((cert.clone(), x509_cert)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,35 +134,77 @@ impl CertificateIdentity {
|
||||
developer_session: &mut DeveloperSession,
|
||||
team: &DeveloperTeam,
|
||||
max_certs_behavior: &MaxCertsBehavior,
|
||||
) -> Result<Self, Report> {
|
||||
) -> Result<(DevelopmentCertificate, X509Certificate), Report> {
|
||||
let csr = Self::build_csr(private_key).context("Failed to generate CSR")?;
|
||||
|
||||
let request = developer_session
|
||||
.submit_development_csr(team, csr, machine_name, None)
|
||||
.await?;
|
||||
let mut i = 0;
|
||||
let mut existing_certs: Option<Vec<DevelopmentCertificate>> = None;
|
||||
|
||||
// TODO: Handle max certs behavior properly instead of just always revoking
|
||||
while i < 4 {
|
||||
i += 1;
|
||||
|
||||
let apple_certs = developer_session.list_ios_certs(team).await?;
|
||||
let result = developer_session
|
||||
.submit_development_csr(team, csr.clone(), machine_name.clone(), None)
|
||||
.await;
|
||||
|
||||
let apple_cert = apple_certs
|
||||
.iter()
|
||||
.find(|c| c.certificate_id == Some(request.cert_request_id.clone()))
|
||||
.ok_or_else(|| report!("Failed to find certificate after submitting CSR"))?;
|
||||
match result {
|
||||
Ok(request) => {
|
||||
let apple_certs = developer_session.list_ios_certs(team).await?;
|
||||
|
||||
let x509_cert = X509Certificate::from_der(
|
||||
apple_cert
|
||||
.cert_content
|
||||
.as_ref()
|
||||
.ok_or_else(|| report!("Certificate content missing"))?
|
||||
.as_ref(),
|
||||
)?;
|
||||
let apple_cert = apple_certs
|
||||
.iter()
|
||||
.find(|c| c.certificate_id == Some(request.cert_request_id.clone()))
|
||||
.ok_or_else(|| {
|
||||
report!("Failed to find certificate after submitting CSR")
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
machine_id: apple_cert.machine_id.clone().unwrap_or_default(),
|
||||
machine_name: apple_cert.machine_name.clone().unwrap_or_default(),
|
||||
certificate: x509_cert,
|
||||
})
|
||||
let x509_cert = X509Certificate::from_der(
|
||||
apple_cert
|
||||
.cert_content
|
||||
.as_ref()
|
||||
.ok_or_else(|| report!("Certificate content missing"))?
|
||||
.as_ref(),
|
||||
)?;
|
||||
|
||||
return Ok((apple_cert.clone(), x509_cert));
|
||||
}
|
||||
Err(e) => {
|
||||
let error = e
|
||||
.iter_reports()
|
||||
.find_map(|node| node.downcast_current_context::<SideloadError>());
|
||||
if let Some(SideloadError::DeveloperError(code, _)) = error {
|
||||
if *code == 7460 {
|
||||
if existing_certs.is_none() {
|
||||
existing_certs = Some(
|
||||
developer_session
|
||||
.list_ios_certs(team)
|
||||
.await?
|
||||
.iter()
|
||||
.filter(|c| c.serial_number.is_some())
|
||||
.cloned()
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
Self::revoke_others(
|
||||
developer_session,
|
||||
team,
|
||||
max_certs_behavior,
|
||||
SideloadError::DeveloperError(
|
||||
*code,
|
||||
"Maximum number of certificates reached".to_string(),
|
||||
),
|
||||
&mut existing_certs.as_mut().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Err(report!("Reached max attempts to request certificate"))
|
||||
}
|
||||
|
||||
fn build_csr(private_key: &RsaPrivateKey) -> Result<String, Report> {
|
||||
@@ -169,4 +225,51 @@ impl CertificateIdentity {
|
||||
|
||||
Ok(params.serialize_request(&subject_key)?.pem()?)
|
||||
}
|
||||
|
||||
async fn revoke_others(
|
||||
developer_session: &mut DeveloperSession,
|
||||
team: &DeveloperTeam,
|
||||
max_certs_behavior: &MaxCertsBehavior,
|
||||
error: SideloadError,
|
||||
existing_certs: &mut Vec<DevelopmentCertificate>,
|
||||
) -> Result<(), Report> {
|
||||
match max_certs_behavior {
|
||||
MaxCertsBehavior::Revoke => {
|
||||
if let Some(cert) = existing_certs.pop() {
|
||||
info!(
|
||||
"Revoking certificate with name: {:?} ({:?})",
|
||||
cert.name, cert.machine_name
|
||||
);
|
||||
developer_session
|
||||
.revoke_development_cert(team, &cert.serial_number.unwrap(), None)
|
||||
.await?;
|
||||
Ok(())
|
||||
} else {
|
||||
error!("No more certificates to revoke but still hitting max certs error");
|
||||
return Err(error.into());
|
||||
}
|
||||
}
|
||||
MaxCertsBehavior::Error => Err(error.into()),
|
||||
MaxCertsBehavior::Prompt(prompt_fn) => {
|
||||
let certs_to_revoke = prompt_fn(existing_certs);
|
||||
if certs_to_revoke.is_none() {
|
||||
error!("User did not select any certificates to revoke");
|
||||
return Err(error.into());
|
||||
}
|
||||
for cert in certs_to_revoke.unwrap() {
|
||||
info!(
|
||||
"Revoking certificate with name: {}",
|
||||
cert.machine_name
|
||||
.unwrap_or(cert.machine_id.unwrap_or_default())
|
||||
);
|
||||
let serial_number = cert.serial_number.clone();
|
||||
developer_session
|
||||
.revoke_development_cert(team, &cert.serial_number.unwrap(), None)
|
||||
.await?;
|
||||
existing_certs.retain(|c| c.serial_number != serial_number);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,32 @@ use crate::util::storage::SideloadingStorage;
|
||||
use keyring::Entry;
|
||||
use rootcause::prelude::*;
|
||||
|
||||
pub struct KeyringStorage {}
|
||||
pub struct KeyringStorage {
|
||||
pub service_name: String,
|
||||
}
|
||||
|
||||
impl KeyringStorage {
|
||||
pub fn new() -> Self {
|
||||
KeyringStorage {}
|
||||
pub fn new(service_name: String) -> Self {
|
||||
KeyringStorage { service_name }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KeyringStorage {
|
||||
fn default() -> Self {
|
||||
KeyringStorage {
|
||||
service_name: "isideload".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SideloadingStorage for KeyringStorage {
|
||||
fn store(&self, key: &str, value: &str) -> Result<(), Report> {
|
||||
Entry::new("isideload", key)?.set_password(value)?;
|
||||
Entry::new(&self.service_name, key)?.set_password(value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn retrieve(&self, key: &str) -> Result<Option<String>, Report> {
|
||||
let entry = Entry::new("isideload", key)?;
|
||||
let entry = Entry::new(&self.service_name, key)?;
|
||||
match entry.get_password() {
|
||||
Ok(password) => Ok(Some(password)),
|
||||
Err(keyring::Error::NoEntry) => Ok(None),
|
||||
@@ -26,11 +36,25 @@ impl SideloadingStorage for KeyringStorage {
|
||||
}
|
||||
|
||||
fn delete(&self, key: &str) -> Result<(), Report> {
|
||||
let entry = Entry::new("isideload", key)?;
|
||||
let entry = Entry::new(&self.service_name, key)?;
|
||||
match entry.delete_credential() {
|
||||
Ok(()) => Ok(()),
|
||||
Err(keyring::Error::NoEntry) => Ok(()),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn store_data(&self, key: &str, value: &[u8]) -> Result<(), Report> {
|
||||
Entry::new(&self.service_name, key)?.set_secret(value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn retrieve_data(&self, key: &str) -> Result<Option<Vec<u8>>, Report> {
|
||||
let entry = Entry::new(&self.service_name, key)?;
|
||||
match entry.get_secret() {
|
||||
Ok(secret) => Ok(Some(secret)),
|
||||
Err(keyring::Error::NoEntry) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
use std::{collections::HashMap, sync::Mutex};
|
||||
|
||||
use base64::prelude::*;
|
||||
use rootcause::prelude::*;
|
||||
|
||||
pub trait SideloadingStorage: Send + Sync {
|
||||
fn store(&self, key: &str, value: &str) -> Result<(), Report>;
|
||||
fn retrieve(&self, key: &str) -> Result<Option<String>, Report>;
|
||||
|
||||
fn store_data(&self, key: &str, value: &[u8]) -> Result<(), Report> {
|
||||
self.store(key, &BASE64_STANDARD.encode(value))
|
||||
}
|
||||
|
||||
fn retrieve_data(&self, key: &str) -> Result<Option<Vec<u8>>, Report> {
|
||||
if let Some(value) = self.retrieve(key)? {
|
||||
Ok(Some(BASE64_STANDARD.decode(value)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(&self, key: &str) -> Result<(), Report> {
|
||||
self.store(key, "")
|
||||
}
|
||||
@@ -13,7 +27,7 @@ pub trait SideloadingStorage: Send + Sync {
|
||||
pub fn new_storage() -> impl SideloadingStorage {
|
||||
#[cfg(feature = "keyring-storage")]
|
||||
{
|
||||
crate::util::keyring_storage::KeyringStorage::new()
|
||||
crate::util::keyring_storage::KeyringStorage::default()
|
||||
}
|
||||
#[cfg(not(feature = "keyring-storage"))]
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user