Sign and install apps

This commit is contained in:
nab138
2026-02-14 01:10:01 -05:00
parent eefbdcafe7
commit 81d79ca11b
12 changed files with 2096 additions and 101 deletions

1717
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,11 @@ This branch is home to isideload-next, the next major version of isideload. It f
Things left todo before the rewrite is considered finished
- Download provisioning profiles
- Signing apps
- Installing apps
(will superceed the original isideload at this point)
- Remove dependency on ring
- More parallelism/cachng for better performance
- Proper entitlement handling
- actually parse macho files and stuff, right now it just uses the bare minimum and applies extra entitlements for livecontainer
- Remove dependency on ring and reduce duplicate dependencies
- partially just need to wait for the rust crypto ecosystem to get through another release cycle
- More parallelism/caching for better performance
## Licensing

View File

@@ -19,7 +19,7 @@ use tracing_subscriber::FmtSubscriber;
async fn main() {
isideload::init().expect("Failed to initialize error reporting");
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::INFO)
.with_max_level(Level::DEBUG)
.finish();
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");

View File

@@ -19,7 +19,7 @@ keyring-storage = ["keyring"]
# Once that becomes stable, hopefuly duplicate dependencies should clean up.\
# Until then, I will wince in pain every time I see how long the output of cargo tree -d is.
[dependencies]
idevice = { version = "0.1.52", optional = true }
idevice = { version = "0.1.52", optional = true, features = ["afc", "installation_proxy"]}
plist = "1.8"
plist-macro = "0.1.4"
reqwest = { version = "0.13.2", features = ["json", "gzip"] }
@@ -50,3 +50,4 @@ x509-certificate = "0.25"
rcgen = { version = "0.14.7", default-features = false, features = ["aws_lc_rs", "pem"] }
p12-keystore = "0.2.0"
zip = { version = "7.4", default-features = false, features = ["deflate"] }
apple-codesign = { path = "../../plume-apple-platform-rs/apple-codesign", default-features = false}

View File

@@ -1,3 +1,4 @@
use idevice::IdeviceError;
use rootcause::{
hooks::{Hooks, context_formatter::ContextFormatterHook},
prelude::*,
@@ -25,6 +26,9 @@ pub enum SideloadError {
#[error("Invalid bundle: {0}")]
InvalidBundle(String),
#[error(transparent)]
IdeviceError(#[from] IdeviceError),
}
// The default reqwest error formatter sucks and provides no info

View File

@@ -5,7 +5,6 @@ use crate::SideloadError;
use crate::dev::app_ids::{AppId, AppIdsApi};
use crate::dev::developer_session::DeveloperSession;
use crate::dev::teams::DeveloperTeam;
use crate::sideload::builder::ExtensionsBehavior;
use crate::sideload::bundle::Bundle;
use crate::sideload::cert_identity::CertificateIdentity;
use rootcause::option_ext::OptionExt;
@@ -13,6 +12,7 @@ use rootcause::prelude::*;
use std::fs::File;
use std::path::PathBuf;
use tokio::io::AsyncWriteExt;
use tracing::info;
use zip::ZipArchive;
pub struct Application {
@@ -81,7 +81,8 @@ impl Application {
}
pub fn get_special_app(&self) -> Option<SpecialApp> {
let special_app = match self.bundle.bundle_identifier().unwrap_or("") {
let bundle_id = self.bundle.bundle_identifier().unwrap_or("");
let special_app = match bundle_id {
"com.rileytestut.AltStore" => Some(SpecialApp::AltStore),
"com.SideStore.SideStore" => Some(SpecialApp::SideStore),
_ => None,
@@ -99,6 +100,10 @@ impl Application {
return Some(SpecialApp::SideStoreLc);
}
if bundle_id == "com.kdt.livecontainer" {
return Some(SpecialApp::LiveContainer);
}
None
}
@@ -154,7 +159,7 @@ impl Application {
pub async fn register_app_ids(
&self,
mode: &ExtensionsBehavior,
//mode: &ExtensionsBehavior,
dev_session: &mut DeveloperSession,
team: &DeveloperTeam,
) -> Result<Vec<AppId>, Report> {
@@ -166,8 +171,7 @@ impl Application {
.list_app_ids(&team, None)
.await
.context("Failed to list app IDs for the developer team")?;
let app_ids_to_register = match mode {
ExtensionsBehavior::RegisterAll => bundles_with_app_id
let app_ids_to_register = bundles_with_app_id
.iter()
.filter(|bundle| {
let bundle_id = bundle.bundle_identifier().unwrap_or("");
@@ -176,9 +180,7 @@ impl Application {
.iter()
.any(|app_id| app_id.identifier == bundle_id)
})
.collect::<Vec<_>>(),
_ => todo!(),
};
.collect::<Vec<_>>();
if let Some(available) = list_app_ids_response.available_quantity
&& app_ids_to_register.len() > available.try_into().unwrap()
@@ -220,10 +222,11 @@ impl Application {
}
let special = special.as_ref().unwrap();
if special == &SpecialApp::SideStoreLc
|| special == &SpecialApp::SideStore
|| special == &SpecialApp::AltStore
{
if matches!(
special,
SpecialApp::SideStoreLc | SpecialApp::SideStore | SpecialApp::AltStore
) {
info!("Injecting certificate for {}", special);
self.bundle.app_info.insert(
"ALTAppGroups".to_string(),
plist::Value::Array(vec![plist::Value::String(group_identifier.to_string())]),
@@ -265,6 +268,20 @@ impl Application {
pub enum SpecialApp {
SideStore,
SideStoreLc,
LiveContainer,
AltStore,
StikStore,
}
// impl display
impl std::fmt::Display for SpecialApp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SpecialApp::SideStore => write!(f, "SideStore"),
SpecialApp::SideStoreLc => write!(f, "SideStore+LiveContainer"),
SpecialApp::LiveContainer => write!(f, "LiveContainer"),
SpecialApp::AltStore => write!(f, "AltStore"),
SpecialApp::StikStore => write!(f, "StikStore"),
}
}
}

View File

@@ -49,34 +49,34 @@ pub enum ExtensionsBehaviorChoice {
RemoveExtensions,
}
/// Behavior used when an app contains sub bundles
pub enum ExtensionsBehavior {
/// Use the main app id/profile for all sub-bundles
ReuseMain,
/// Create separate app ids/profiles for each sub-bundle
RegisterAll,
/// Remove all sub-bundles
RemoveExtensions,
/// Prompt the user to choose one of the above behaviors
Prompt(fn(&Vec<String>) -> ExtensionsBehaviorChoice),
}
// /// Behavior used when an app contains sub bundles
// pub enum ExtensionsBehavior {
// /// Use the main app id/profile for all sub-bundles
// ReuseMain,
// /// Create separate app ids/profiles for each sub-bundle
// RegisterAll,
// /// Remove all sub-bundles
// RemoveExtensions,
// /// Prompt the user to choose one of the above behaviors
// Prompt(fn(&Vec<String>) -> ExtensionsBehaviorChoice),
// }
impl From<ExtensionsBehaviorChoice> for ExtensionsBehavior {
fn from(choice: ExtensionsBehaviorChoice) -> Self {
match choice {
ExtensionsBehaviorChoice::ReuseMain => ExtensionsBehavior::ReuseMain,
ExtensionsBehaviorChoice::RegisterAll => ExtensionsBehavior::RegisterAll,
ExtensionsBehaviorChoice::RemoveExtensions => ExtensionsBehavior::RemoveExtensions,
}
}
}
// impl From<ExtensionsBehaviorChoice> for ExtensionsBehavior {
// fn from(choice: ExtensionsBehaviorChoice) -> Self {
// match choice {
// ExtensionsBehaviorChoice::ReuseMain => ExtensionsBehavior::ReuseMain,
// ExtensionsBehaviorChoice::RegisterAll => ExtensionsBehavior::RegisterAll,
// ExtensionsBehaviorChoice::RemoveExtensions => ExtensionsBehavior::RemoveExtensions,
// }
// }
// }
pub struct SideloaderBuilder {
developer_session: DeveloperSession,
apple_email: String,
team_selection: Option<TeamSelection>,
max_certs_behavior: Option<MaxCertsBehavior>,
extensions_behavior: Option<ExtensionsBehavior>,
//extensions_behavior: Option<ExtensionsBehavior>,
storage: Option<Box<dyn SideloadingStorage>>,
machine_name: Option<String>,
}
@@ -90,7 +90,7 @@ impl SideloaderBuilder {
machine_name: None,
apple_email,
max_certs_behavior: None,
extensions_behavior: None,
// extensions_behavior: None,
}
}
@@ -128,10 +128,10 @@ impl SideloaderBuilder {
self
}
pub fn extensions_behavior(mut self, behavior: ExtensionsBehavior) -> Self {
self.extensions_behavior = Some(behavior);
self
}
// pub fn extensions_behavior(mut self, behavior: ExtensionsBehavior) -> Self {
// self.extensions_behavior = Some(behavior);
// self
// }
/// Build the `Sideloader` instance with the provided configuration
pub fn build(self) -> Sideloader {
@@ -143,8 +143,8 @@ impl SideloaderBuilder {
self.machine_name.unwrap_or_else(|| "isideload".to_string()),
self.storage
.unwrap_or_else(|| Box::new(crate::util::storage::new_storage())),
self.extensions_behavior
.unwrap_or(ExtensionsBehavior::RegisterAll),
// self.extensions_behavior
// .unwrap_or(ExtensionsBehavior::RegisterAll),
)
}
}

View File

@@ -10,7 +10,7 @@ use std::{
use crate::SideloadError;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Bundle {
pub app_info: Dictionary,
pub bundle_dir: PathBuf,
@@ -134,6 +134,50 @@ impl Bundle {
)?;
Ok(())
}
fn from_dylib_path(dylib_path: PathBuf) -> Self {
Self {
app_info: Dictionary::new(),
bundle_dir: dylib_path,
app_extensions: Vec::new(),
frameworks: Vec::new(),
_libraries: Vec::new(),
}
}
fn collect_dylib_bundles(&self) -> Vec<Bundle> {
self._libraries
.iter()
.map(|relative| Self::from_dylib_path(self.bundle_dir.join(relative)))
.collect()
}
fn collect_nested_bundles_into(&self, bundles: &mut Vec<Bundle>) {
for bundle in &self.app_extensions {
bundles.push(bundle.clone());
bundle.collect_nested_bundles_into(bundles);
}
for bundle in &self.frameworks {
bundles.push(bundle.clone());
bundle.collect_nested_bundles_into(bundles);
}
}
pub fn collect_nested_bundles(&self) -> Vec<Bundle> {
let mut bundles = Vec::new();
self.collect_nested_bundles_into(&mut bundles);
bundles.extend(self.collect_dylib_bundles());
bundles
}
pub fn collect_bundles_sorted(&self) -> Vec<Bundle> {
let mut bundles = self.collect_nested_bundles();
bundles.push(self.clone());
bundles.sort_by_key(|b| b.bundle_dir.components().count());
bundles.reverse();
bundles
}
}
fn assert_bundle(condition: bool, msg: &str) -> Result<(), Report> {

View File

@@ -1,3 +1,7 @@
use apple_codesign::{
SigningSettings,
cryptography::{InMemoryPrivateKey, PrivateKey},
};
use hex::ToHex;
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair, PKCS_RSA_SHA256};
use rootcause::prelude::*;
@@ -9,7 +13,7 @@ use rsa::{
use sha2::{Digest, Sha256};
use tracing::{error, info};
use x509_certificate::X509Certificate;
use x509_certificate::CapturedX509Certificate;
use crate::{
SideloadError,
@@ -25,8 +29,9 @@ use crate::{
pub struct CertificateIdentity {
pub machine_id: String,
pub machine_name: String,
pub certificate: X509Certificate,
pub certificate: CapturedX509Certificate,
pub private_key: RsaPrivateKey,
pub signing_key: InMemoryPrivateKey,
}
impl CertificateIdentity {
@@ -63,7 +68,7 @@ impl CertificateIdentity {
pub fn get_serial_number(&self) -> String {
let serial: String = self.certificate.serial_number_asn1().encode_hex();
serial.trim_start_matches('0').to_string()
serial.trim_start_matches('0').to_string().to_uppercase()
}
pub async fn retrieve(
@@ -75,6 +80,7 @@ impl CertificateIdentity {
max_certs_behavior: &MaxCertsBehavior,
) -> Result<Self, Report> {
let pr = Self::retrieve_private_key(apple_email, storage).await?;
let signing_key = Self::build_signing_key(&pr)?;
let found = Self::find_matching(&pr, machine_name, developer_session, team).await;
if let Ok(Some((cert, x509_cert))) = found {
@@ -84,6 +90,7 @@ impl CertificateIdentity {
machine_name: cert.machine_name.clone().unwrap_or_default(),
certificate: x509_cert,
private_key: pr,
signing_key,
});
}
@@ -107,6 +114,7 @@ impl CertificateIdentity {
machine_name: cert.machine_name.clone().unwrap_or_default(),
certificate: x509_cert,
private_key: pr,
signing_key,
})
}
@@ -139,7 +147,7 @@ impl CertificateIdentity {
machine_name: &str,
developer_session: &mut DeveloperSession,
team: &DeveloperTeam,
) -> Result<Option<(DevelopmentCertificate, X509Certificate)>, Report> {
) -> Result<Option<(DevelopmentCertificate, CapturedX509Certificate)>, Report> {
let public_key_der = private_key
.to_public_key()
.to_pkcs1_der()?
@@ -156,7 +164,7 @@ impl CertificateIdentity {
})
{
let x509_cert =
X509Certificate::from_der(cert.cert_content.as_ref().unwrap().as_ref())?;
CapturedX509Certificate::from_der(cert.cert_content.as_ref().unwrap().as_ref())?;
if public_key_der == x509_cert.public_key_data().as_ref() {
return Ok(Some((cert.clone(), x509_cert)));
@@ -172,7 +180,7 @@ impl CertificateIdentity {
developer_session: &mut DeveloperSession,
team: &DeveloperTeam,
max_certs_behavior: &MaxCertsBehavior,
) -> Result<(DevelopmentCertificate, X509Certificate), Report> {
) -> Result<(DevelopmentCertificate, CapturedX509Certificate), Report> {
let csr = Self::build_csr(private_key).context("Failed to generate CSR")?;
let mut i = 0;
@@ -196,7 +204,7 @@ impl CertificateIdentity {
report!("Failed to find certificate after submitting CSR")
})?;
let x509_cert = X509Certificate::from_der(
let x509_cert = CapturedX509Certificate::from_der(
apple_cert
.cert_content
.as_ref()
@@ -264,6 +272,11 @@ impl CertificateIdentity {
Ok(params.serialize_request(&subject_key)?.pem()?)
}
fn build_signing_key(private_key: &RsaPrivateKey) -> Result<InMemoryPrivateKey, Report> {
let pkcs8 = private_key.to_pkcs8_der()?;
Ok(InMemoryPrivateKey::from_pkcs8_der(pkcs8.as_bytes())?)
}
async fn revoke_others(
developer_session: &mut DeveloperSession,
team: &DeveloperTeam,
@@ -310,4 +323,18 @@ impl CertificateIdentity {
}
}
}
pub fn setup_signing_settings<'a>(
&'a self,
settings: &mut SigningSettings<'a>,
) -> Result<(), Report> {
settings.set_signing_key(
self.signing_key.as_key_info_signer(),
self.certificate.clone(),
);
settings.chain_apple_certificates();
settings.set_team_id_from_signing_certificate();
Ok(())
}
}

View File

@@ -0,0 +1,95 @@
use idevice::{
IdeviceService, afc::AfcClient, installation_proxy::InstallationProxyClient,
provider::IdeviceProvider,
};
use plist_macro::plist;
use rootcause::prelude::*;
use crate::SideloadError as Error;
use std::pin::Pin;
use std::{future::Future, path::Path};
/// Installs an ***already signed*** app onto your device.
/// To sign and install an app, see [`crate::sideload::sideload_app`]
pub async fn install_app(
provider: &impl IdeviceProvider,
app_path: &Path,
progress_callback: impl Fn(u64),
) -> Result<(), Report> {
let mut afc_client = AfcClient::connect(provider)
.await
.map_err(Error::IdeviceError)?;
let dir = format!(
"PublicStaging/{}",
app_path.file_name().unwrap().to_string_lossy()
);
afc_upload_dir(&mut afc_client, app_path, &dir).await?;
let mut instproxy_client = InstallationProxyClient::connect(provider)
.await
.map_err(Error::IdeviceError)?;
let options = plist!(dict {
"PackageType": "Developer"
});
instproxy_client
.install_with_callback(
dir,
Some(plist::Value::Dictionary(options)),
async |(percentage, _)| {
progress_callback(percentage);
},
(),
)
.await
.map_err(Error::IdeviceError)?;
Ok(())
}
fn afc_upload_dir<'a>(
afc_client: &'a mut AfcClient,
path: &'a Path,
afc_path: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), Report>> + Send + 'a>> {
Box::pin(async move {
let entries = std::fs::read_dir(path)?;
afc_client
.mk_dir(afc_path)
.await
.map_err(Error::IdeviceError)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let new_afc_path = format!(
"{}/{}",
afc_path,
path.file_name().unwrap().to_string_lossy()
);
afc_upload_dir(afc_client, &path, &new_afc_path).await?;
} else {
let mut file_handle = afc_client
.open(
format!(
"{}/{}",
afc_path,
path.file_name().unwrap().to_string_lossy()
),
idevice::afc::opcode::AfcFopenMode::WrOnly,
)
.await
.map_err(Error::IdeviceError)?;
let bytes = std::fs::read(&path)?;
file_handle
.write_entire(&bytes)
.await
.map_err(Error::IdeviceError)?;
file_handle.close().await.map_err(Error::IdeviceError)?;
}
}
Ok(())
})
}

View File

@@ -2,5 +2,7 @@ pub mod application;
pub mod builder;
pub mod bundle;
pub mod cert_identity;
#[cfg(feature = "install")]
pub mod install;
pub mod sideloader;
pub use builder::{SideloaderBuilder, TeamSelection};

View File

@@ -9,16 +9,19 @@ use crate::{
sideload::{
TeamSelection,
application::{Application, SpecialApp},
builder::{ExtensionsBehavior, MaxCertsBehavior},
builder::MaxCertsBehavior,
cert_identity::CertificateIdentity,
},
util::{device::IdeviceInfo, storage::SideloadingStorage},
util::{device::IdeviceInfo, plist::PlistDataExtract, storage::SideloadingStorage},
};
use std::path::PathBuf;
use apple_codesign::{SigningSettings, UnifiedSigner};
use idevice::provider::IdeviceProvider;
use rootcause::prelude::*;
use plist::Dictionary;
use plist_macro::plist_to_xml_string;
use rootcause::{option_ext::OptionExt, prelude::*};
use tracing::info;
pub struct Sideloader {
@@ -28,7 +31,7 @@ pub struct Sideloader {
machine_name: String,
apple_email: String,
max_certs_behavior: MaxCertsBehavior,
extensions_behavior: ExtensionsBehavior,
//extensions_behavior: ExtensionsBehavior,
}
impl Sideloader {
@@ -42,7 +45,7 @@ impl Sideloader {
max_certs_behavior: MaxCertsBehavior,
machine_name: String,
storage: Box<dyn SideloadingStorage>,
extensions_behavior: ExtensionsBehavior,
//extensions_behavior: ExtensionsBehavior,
) -> Self {
Sideloader {
team_selection,
@@ -51,24 +54,20 @@ impl Sideloader {
machine_name,
apple_email,
max_certs_behavior,
extensions_behavior,
//extensions_behavior,
}
}
/// Sign and install an app
pub async fn install_app(
pub async fn sign_app(
&mut self,
device_provider: &impl IdeviceProvider,
app_path: PathBuf,
) -> Result<(), Report> {
let device_info = IdeviceInfo::from_device(device_provider).await?;
let team = self.get_team().await?;
self.dev_session
.ensure_device_registered(&team, &device_info.name, &device_info.udid, None)
.await?;
team: Option<DeveloperTeam>,
) -> Result<PathBuf, Report> {
let team = match team {
Some(t) => t,
None => self.get_team().await?,
};
let cert_identity = CertificateIdentity::retrieve(
&self.machine_name,
&self.apple_email,
@@ -87,7 +86,10 @@ impl Sideloader {
let main_app_id_str = format!("{}.{}", main_bundle_id, team.team_id);
app.update_bundle_id(&main_bundle_id, &main_app_id_str)?;
let mut app_ids = app
.register_app_ids(&self.extensions_behavior, &mut self.dev_session, &team)
.register_app_ids(
/*&self.extensions_behavior, */ &mut self.dev_session,
&team,
)
.await?;
let main_app_id = match app_ids
.iter()
@@ -130,7 +132,8 @@ impl Sideloader {
}
app.apply_special_app_behavior(&special, &group_identifier, &cert_identity)
.await?;
.await
.context("Failed to modify app bundle")?;
let provisioning_profile = self
.dev_session
@@ -145,9 +148,67 @@ impl Sideloader {
ext.write_info()?;
}
Ok(())
tokio::fs::write(
app.bundle.bundle_dir.join("embedded.mobileprovision"),
provisioning_profile.encoded_profile.as_ref(),
)
.await?;
let mut settings = Self::signing_settings(&cert_identity)?;
let entitlements: Dictionary = Self::entitlements_from_prov(
provisioning_profile.encoded_profile.as_ref(),
&special,
&team,
)?;
settings
.set_entitlements_xml(
apple_codesign::SettingsScope::Main,
plist_to_xml_string(&entitlements),
)
.context("Failed to set entitlements XML")?;
let signer = UnifiedSigner::new(settings);
for bundle in app.bundle.collect_bundles_sorted() {
info!("Signing bundle {}", bundle.bundle_dir.display());
signer
.sign_path_in_place(&bundle.bundle_dir)
.context(format!(
"Failed to sign bundle: {}",
bundle.bundle_dir.display()
))?;
}
info!("App signed!");
Ok(app.bundle.bundle_dir.clone())
}
#[cfg(feature = "install")]
pub async fn install_app(
&mut self,
device_provider: &impl IdeviceProvider,
app_path: PathBuf,
) -> Result<(), Report> {
let device_info = IdeviceInfo::from_device(device_provider).await?;
let team = self.get_team().await?;
self.dev_session
.ensure_device_registered(&team, &device_info.name, &device_info.udid, None)
.await?;
let signed_app_path = self.sign_app(app_path, Some(team)).await?;
info!("Installing...");
crate::sideload::install::install_app(device_provider, &signed_app_path, |progress| {
info!("Installing: {}%", progress);
})
.await
.context("Failed to install app on device")?;
Ok(())
}
/// Get the developer team according to the configured team selection behavior
pub async fn get_team(&mut self) -> Result<DeveloperTeam, Report> {
let teams = self.dev_session.list_teams().await?;
@@ -175,4 +236,64 @@ impl Sideloader {
}
})
}
pub fn signing_settings<'a>(
cert: &'a CertificateIdentity,
) -> Result<SigningSettings<'a>, Report> {
let mut settings = SigningSettings::default();
cert.setup_signing_settings(&mut settings)?;
settings.set_for_notarization(false);
settings.set_shallow(true);
Ok(settings)
}
fn entitlements_from_prov(
data: &[u8],
special: &Option<SpecialApp>,
team: &DeveloperTeam,
) -> Result<Dictionary, Report> {
let start = data
.windows(6)
.position(|w| w == b"<plist")
.ok_or_report()?;
let end = data
.windows(8)
.rposition(|w| w == b"</plist>")
.ok_or_report()?
+ 8;
let plist_data = &data[start..end];
let plist = plist::Value::from_reader_xml(plist_data)?;
let mut entitlements = plist
.as_dictionary()
.ok_or_report()?
.get_dict("Entitlements")?
.clone();
if matches!(
special,
Some(SpecialApp::SideStoreLc) | Some(SpecialApp::LiveContainer)
) {
let mut keychain_access = vec![plist::Value::String(format!(
"{}.com.kdt.livecontainer.shared",
team.team_id
))];
for number in 1..128 {
keychain_access.push(plist::Value::String(format!(
"{}.com.kdt.livecontainer.shared.{}",
team.team_id, number
)));
}
entitlements.insert(
"keychain-access-groups".to_string(),
plist::Value::Array(keychain_access),
);
}
Ok(entitlements)
}
}