Implement cert revoke and prompt modes

This commit is contained in:
nab138
2026-02-09 09:24:44 -05:00
parent ec9af89d74
commit 78eb7fdfcc
8 changed files with 259 additions and 73 deletions

8
Cargo.lock generated
View File

@@ -1793,9 +1793,9 @@ dependencies = [
[[package]] [[package]]
name = "rootcause" name = "rootcause"
version = "0.11.1" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a751633dcb95a6b1c954f0fa15c2afd9b4802640f8045432f68a1f4bde4b871" checksum = "03621279b1bafd0cd806d4a4e301530bfab4a54a9a572ea45a4fe5072c3e134b"
dependencies = [ dependencies = [
"hashbrown 0.16.1", "hashbrown 0.16.1",
"indexmap", "indexmap",
@@ -1806,9 +1806,9 @@ dependencies = [
[[package]] [[package]]
name = "rootcause-internals" name = "rootcause-internals"
version = "0.11.1" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9eeddca0d656f1a58ce3fc3f41b0b877a7e760460108712ad39b60181fdcb3e" checksum = "2a6575ad7db4a6f026820de38c377b3e06fc59ceac225f868dfede39cd70e432"
dependencies = [ dependencies = [
"triomphe", "triomphe",
] ]

View File

@@ -4,8 +4,11 @@ use idevice::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection};
use isideload::{ use isideload::{
anisette::remote_v3::RemoteV3AnisetteProvider, anisette::remote_v3::RemoteV3AnisetteProvider,
auth::apple_account::AppleAccount, auth::apple_account::AppleAccount,
dev::developer_session::DeveloperSession, dev::{
sideload::{SideloaderBuilder, TeamSelection}, certificates::DevelopmentCertificate, developer_session::DeveloperSession,
teams::DeveloperTeam,
},
sideload::{SideloaderBuilder, TeamSelection, builder::MaxCertsBehavior},
}; };
use tracing::Level; use tracing::Level;
@@ -70,25 +73,58 @@ async fn main() {
.unwrap() .unwrap()
.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "isideload-demo"); .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()) let mut sideloader = SideloaderBuilder::new(dev_session, apple_id.to_string())
.team_selection(TeamSelection::Prompt(|teams| { .team_selection(TeamSelection::Prompt(team_selection_prompt))
println!("Please select a team:"); .max_certs_behavior(MaxCertsBehavior::Prompt(cert_selection_prompt))
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())
}))
.build(); .build();
let result = sideloader.install_app(&provider, app_path).await; let result = sideloader.install_app(&provider, app_path).await;

View File

@@ -27,7 +27,7 @@ rand = "0.10.0"
uuid = {version = "1.20.0", features = ["v4"] } uuid = {version = "1.20.0", features = ["v4"] }
tracing = "0.1.44" tracing = "0.1.44"
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots"] } tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots"] }
rootcause = "0.11.1" rootcause = "0.12.0"
futures-util = "0.3.31" futures-util = "0.3.31"
serde_json = "1.0.149" serde_json = "1.0.149"
base64 = "0.22.1" base64 = "0.22.1"

View File

@@ -63,7 +63,6 @@ impl AppleAccount {
anisette_generator: AnisetteDataGenerator, anisette_generator: AnisetteDataGenerator,
debug: bool, debug: bool,
) -> Result<Self, Report> { ) -> Result<Self, Report> {
info!("Initializing apple account");
if debug { if debug {
warn!("Debug mode enabled: this is a security risk!"); warn!("Debug mode enabled: this is a security risk!");
} }

View File

@@ -1,7 +1,10 @@
use std::fmt::Display; use std::fmt::Display;
use crate::{ use crate::{
dev::{developer_session::DeveloperSession, teams::DeveloperTeam}, dev::{
certificates::DevelopmentCertificate, developer_session::DeveloperSession,
teams::DeveloperTeam,
},
sideload::sideloader::Sideloader, sideload::sideloader::Sideloader,
util::storage::SideloadingStorage, util::storage::SideloadingStorage,
}; };
@@ -27,10 +30,12 @@ impl Display for TeamSelection {
} }
pub enum MaxCertsBehavior { 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, Revoke,
/// If the maximum number of certificates is reached, return an error instead of creating a new certificate /// If the maximum number of certificates is reached, return an error instead of creating a new certificate
Error, 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 { pub struct SideloaderBuilder {
@@ -69,12 +74,17 @@ impl SideloaderBuilder {
self self
} }
pub fn max_certs_behavior(mut self, behavior: MaxCertsBehavior) -> Self {
self.max_certs_behavior = Some(behavior);
self
}
pub fn build(self) -> Sideloader { pub fn build(self) -> Sideloader {
Sideloader::new( Sideloader::new(
self.developer_session, self.developer_session,
self.apple_email, self.apple_email,
self.team_selection.unwrap_or(TeamSelection::First), 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.machine_name.unwrap_or_else(|| "isideload".to_string()),
self.storage self.storage
.unwrap_or_else(|| Box::new(crate::util::storage::new_storage())), .unwrap_or_else(|| Box::new(crate::util::storage::new_storage())),

View File

@@ -11,8 +11,11 @@ use tracing::{error, info};
use x509_certificate::X509Certificate; use x509_certificate::X509Certificate;
use crate::{ use crate::{
SideloadError,
dev::{ dev::{
certificates::CertificatesApi, developer_session::DeveloperSession, teams::DeveloperTeam, certificates::{CertificatesApi, DevelopmentCertificate},
developer_session::DeveloperSession,
teams::DeveloperTeam,
}, },
sideload::builder::MaxCertsBehavior, sideload::builder::MaxCertsBehavior,
util::storage::SideloadingStorage, util::storage::SideloadingStorage,
@@ -22,6 +25,7 @@ pub struct CertificateIdentity {
pub machine_id: String, pub machine_id: String,
pub machine_name: String, pub machine_name: String,
pub certificate: X509Certificate, pub certificate: X509Certificate,
pub private_key: RsaPrivateKey,
} }
impl CertificateIdentity { impl CertificateIdentity {
@@ -36,23 +40,37 @@ impl CertificateIdentity {
let pr = Self::retrieve_private_key(apple_email, storage).await?; let pr = Self::retrieve_private_key(apple_email, storage).await?;
let found = Self::find_matching(&pr, machine_name, developer_session, team).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"); 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 { if let Err(e) = found {
error!("Failed to check for matching certificate: {:?}", e); error!("Failed to check for matching certificate: {:?}", e);
} }
info!("Requesting new certificate"); info!("Requesting new certificate");
Self::request_certificate( let (cert, x509_cert) = Self::request_certificate(
&pr, &pr,
machine_name.to_string(), machine_name.to_string(),
developer_session, developer_session,
team, team,
max_certs_behavior, 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( async fn retrieve_private_key(
@@ -63,16 +81,16 @@ impl CertificateIdentity {
hasher.update(apple_email.as_bytes()); hasher.update(apple_email.as_bytes());
let email_hash = hex::encode(hasher.finalize()); 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() { 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 mut rng = rand::rng();
let private_key = RsaPrivateKey::new(&mut rng, 2048)?; let private_key = RsaPrivateKey::new(&mut rng, 2048)?;
storage.store( storage.store_data(
&format!("{}/key", email_hash), &format!("{}/key", email_hash),
&private_key.to_pkcs8_pem(Default::default())?.to_string(), &private_key.to_pkcs8_der()?.as_bytes(),
)?; )?;
Ok(private_key) Ok(private_key)
@@ -83,7 +101,7 @@ impl CertificateIdentity {
machine_name: &str, machine_name: &str,
developer_session: &mut DeveloperSession, developer_session: &mut DeveloperSession,
team: &DeveloperTeam, team: &DeveloperTeam,
) -> Result<Option<Self>, Report> { ) -> Result<Option<(DevelopmentCertificate, X509Certificate)>, Report> {
let public_key_der = private_key let public_key_der = private_key
.to_public_key() .to_public_key()
.to_pkcs1_der()? .to_pkcs1_der()?
@@ -103,11 +121,7 @@ impl CertificateIdentity {
X509Certificate::from_der(cert.cert_content.as_ref().unwrap().as_ref())?; X509Certificate::from_der(cert.cert_content.as_ref().unwrap().as_ref())?;
if public_key_der == x509_cert.public_key_data().as_ref() { if public_key_der == x509_cert.public_key_data().as_ref() {
return Ok(Some(Self { return Ok(Some((cert.clone(), x509_cert)));
machine_id: cert.machine_id.clone().unwrap_or_default(),
machine_name: cert.machine_name.clone().unwrap_or_default(),
certificate: x509_cert,
}));
} }
} }
@@ -120,35 +134,77 @@ impl CertificateIdentity {
developer_session: &mut DeveloperSession, developer_session: &mut DeveloperSession,
team: &DeveloperTeam, team: &DeveloperTeam,
max_certs_behavior: &MaxCertsBehavior, max_certs_behavior: &MaxCertsBehavior,
) -> Result<Self, Report> { ) -> Result<(DevelopmentCertificate, X509Certificate), Report> {
let csr = Self::build_csr(private_key).context("Failed to generate CSR")?; let csr = Self::build_csr(private_key).context("Failed to generate CSR")?;
let request = developer_session let mut i = 0;
.submit_development_csr(team, csr, machine_name, None) let mut existing_certs: Option<Vec<DevelopmentCertificate>> = None;
.await?;
// 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 match result {
.iter() Ok(request) => {
.find(|c| c.certificate_id == Some(request.cert_request_id.clone())) let apple_certs = developer_session.list_ios_certs(team).await?;
.ok_or_else(|| report!("Failed to find certificate after submitting CSR"))?;
let x509_cert = X509Certificate::from_der( let apple_cert = apple_certs
apple_cert .iter()
.cert_content .find(|c| c.certificate_id == Some(request.cert_request_id.clone()))
.as_ref() .ok_or_else(|| {
.ok_or_else(|| report!("Certificate content missing"))? report!("Failed to find certificate after submitting CSR")
.as_ref(), })?;
)?;
Ok(Self { let x509_cert = X509Certificate::from_der(
machine_id: apple_cert.machine_id.clone().unwrap_or_default(), apple_cert
machine_name: apple_cert.machine_name.clone().unwrap_or_default(), .cert_content
certificate: x509_cert, .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> { fn build_csr(private_key: &RsaPrivateKey) -> Result<String, Report> {
@@ -169,4 +225,51 @@ impl CertificateIdentity {
Ok(params.serialize_request(&subject_key)?.pem()?) 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(())
}
}
}
} }

View File

@@ -2,22 +2,32 @@ use crate::util::storage::SideloadingStorage;
use keyring::Entry; use keyring::Entry;
use rootcause::prelude::*; use rootcause::prelude::*;
pub struct KeyringStorage {} pub struct KeyringStorage {
pub service_name: String,
}
impl KeyringStorage { impl KeyringStorage {
pub fn new() -> Self { pub fn new(service_name: String) -> Self {
KeyringStorage {} KeyringStorage { service_name }
}
}
impl Default for KeyringStorage {
fn default() -> Self {
KeyringStorage {
service_name: "isideload".to_string(),
}
} }
} }
impl SideloadingStorage for KeyringStorage { impl SideloadingStorage for KeyringStorage {
fn store(&self, key: &str, value: &str) -> Result<(), Report> { 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(()) Ok(())
} }
fn retrieve(&self, key: &str) -> Result<Option<String>, Report> { 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() { match entry.get_password() {
Ok(password) => Ok(Some(password)), Ok(password) => Ok(Some(password)),
Err(keyring::Error::NoEntry) => Ok(None), Err(keyring::Error::NoEntry) => Ok(None),
@@ -26,11 +36,25 @@ impl SideloadingStorage for KeyringStorage {
} }
fn delete(&self, key: &str) -> Result<(), Report> { 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() { match entry.delete_credential() {
Ok(()) => Ok(()), Ok(()) => Ok(()),
Err(keyring::Error::NoEntry) => Ok(()), Err(keyring::Error::NoEntry) => Ok(()),
Err(e) => Err(e.into()), 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()),
}
}
} }

View File

@@ -1,10 +1,24 @@
use std::{collections::HashMap, sync::Mutex}; use std::{collections::HashMap, sync::Mutex};
use base64::prelude::*;
use rootcause::prelude::*; use rootcause::prelude::*;
pub trait SideloadingStorage: Send + Sync { pub trait SideloadingStorage: Send + Sync {
fn store(&self, key: &str, value: &str) -> Result<(), Report>; fn store(&self, key: &str, value: &str) -> Result<(), Report>;
fn retrieve(&self, key: &str) -> Result<Option<String>, 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> { fn delete(&self, key: &str) -> Result<(), Report> {
self.store(key, "") self.store(key, "")
} }
@@ -13,7 +27,7 @@ pub trait SideloadingStorage: Send + Sync {
pub fn new_storage() -> impl SideloadingStorage { pub fn new_storage() -> impl SideloadingStorage {
#[cfg(feature = "keyring-storage")] #[cfg(feature = "keyring-storage")]
{ {
crate::util::keyring_storage::KeyringStorage::new() crate::util::keyring_storage::KeyringStorage::default()
} }
#[cfg(not(feature = "keyring-storage"))] #[cfg(not(feature = "keyring-storage"))]
{ {