use {
crate::{
certificate::AppleCertificate,
code_directory::{CodeSignatureFlags, ExecutableSegmentFlags},
code_requirement::CodeRequirementExpression,
error::AppleCodesignError,
macho::{Blob, DigestType, RequirementBlob},
},
goblin::mach::cputype::{
CpuType, CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_ARM64_32, CPU_TYPE_X86_64,
},
reqwest::{IntoUrl, Url},
std::{collections::BTreeMap, convert::TryFrom, fmt::Formatter},
x509_certificate::{CapturedX509Certificate, InMemorySigningKeyPair},
};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SettingsScope {
Main,
Path(String),
MultiArchIndex(usize),
MultiArchCpuType(CpuType),
PathMultiArchIndex(String, usize),
PathMultiArchCpuType(String, CpuType),
}
impl std::fmt::Display for SettingsScope {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Main => f.write_str("main signing target"),
Self::Path(path) => f.write_fmt(format_args!("path {}", path)),
Self::MultiArchIndex(index) => f.write_fmt(format_args!(
"fat/universal Mach-O binaries at index {}",
index
)),
Self::MultiArchCpuType(cpu_type) => f.write_fmt(format_args!(
"fat/universal Mach-O binaries for CPU {}",
cpu_type
)),
Self::PathMultiArchIndex(path, index) => f.write_fmt(format_args!(
"fat/universal Mach-O binaries at index {} under path {}",
index, path
)),
Self::PathMultiArchCpuType(path, cpu_type) => f.write_fmt(format_args!(
"fat/universal Mach-O binaries for CPU {} under path {}",
cpu_type, path
)),
}
}
}
impl SettingsScope {
fn parse_at_expr(
at_expr: &str,
) -> Result<(Option<usize>, Option<CpuType>), AppleCodesignError> {
match at_expr.parse::<usize>() {
Ok(index) => Ok((Some(index), None)),
Err(_) => {
if at_expr.starts_with('[') && at_expr.ends_with(']') {
let v = &at_expr[1..at_expr.len() - 1];
let parts = v.split('=').collect::<Vec<_>>();
if parts.len() == 2 {
let (key, value) = (parts[0], parts[1]);
if key != "cpu_type" {
return Err(AppleCodesignError::ParseSettingsScope(format!(
"in '@{}', {} not recognized; must be cpu_type",
at_expr, key
)));
}
if let Some(cpu_type) = match value {
"arm" => Some(CPU_TYPE_ARM),
"arm64" => Some(CPU_TYPE_ARM64),
"arm64_32" => Some(CPU_TYPE_ARM64_32),
"x86_64" => Some(CPU_TYPE_X86_64),
_ => None,
} {
return Ok((None, Some(cpu_type)));
}
match value.parse::<u32>() {
Ok(cpu_type) => Ok((None, Some(cpu_type as CpuType))),
Err(_) => Err(AppleCodesignError::ParseSettingsScope(format!(
"in '@{}', cpu_arch value {} not recognized",
at_expr, value
))),
}
} else {
Err(AppleCodesignError::ParseSettingsScope(format!(
"'{}' sub-expression isn't of form <key>=<value>",
v
)))
}
} else {
Err(AppleCodesignError::ParseSettingsScope(format!(
"in '{}', @ expression not recognized",
at_expr
)))
}
}
}
}
}
impl AsRef<SettingsScope> for SettingsScope {
fn as_ref(&self) -> &SettingsScope {
self
}
}
impl TryFrom<&str> for SettingsScope {
type Error = AppleCodesignError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
if s == "@main" {
Ok(Self::Main)
} else if let Some(at_expr) = s.strip_prefix('@') {
match Self::parse_at_expr(at_expr)? {
(Some(index), None) => Ok(Self::MultiArchIndex(index)),
(None, Some(cpu_type)) => Ok(Self::MultiArchCpuType(cpu_type)),
_ => panic!("this shouldn't happen"),
}
} else {
let parts = s.rsplitn(2, '@').collect::<Vec<_>>();
match parts.len() {
1 => Ok(Self::Path(s.to_string())),
2 => {
let (at_expr, path) = (parts[0], parts[1]);
match Self::parse_at_expr(at_expr)? {
(Some(index), None) => {
Ok(Self::PathMultiArchIndex(path.to_string(), index))
}
(None, Some(cpu_type)) => {
Ok(Self::PathMultiArchCpuType(path.to_string(), cpu_type))
}
_ => panic!("this shouldn't happen"),
}
}
_ => panic!("this shouldn't happen"),
}
}
}
}
#[derive(Clone, Debug)]
pub enum DesignatedRequirementMode {
Auto,
Explicit(Vec<Vec<u8>>),
}
#[derive(Clone, Debug, Default)]
pub struct SigningSettings<'key> {
signing_key: Option<(&'key InMemorySigningKeyPair, CapturedX509Certificate)>,
certificates: Vec<CapturedX509Certificate>,
time_stamp_url: Option<Url>,
team_id: Option<String>,
digest_type: DigestType,
identifiers: BTreeMap<SettingsScope, String>,
entitlements: BTreeMap<SettingsScope, String>,
designated_requirement: BTreeMap<SettingsScope, DesignatedRequirementMode>,
code_signature_flags: BTreeMap<SettingsScope, CodeSignatureFlags>,
executable_segment_flags: BTreeMap<SettingsScope, ExecutableSegmentFlags>,
info_plist_data: BTreeMap<SettingsScope, Vec<u8>>,
code_resources_data: BTreeMap<SettingsScope, Vec<u8>>,
}
impl<'key> SigningSettings<'key> {
pub fn digest_type(&self) -> &DigestType {
&self.digest_type
}
pub fn set_digest_type(&mut self, digest_type: DigestType) {
self.digest_type = digest_type;
}
pub fn signing_key(&self) -> Option<&(&'key InMemorySigningKeyPair, CapturedX509Certificate)> {
self.signing_key.as_ref()
}
pub fn set_signing_key(
&mut self,
private: &'key InMemorySigningKeyPair,
public: CapturedX509Certificate,
) {
self.signing_key = Some((private, public));
}
pub fn certificate_chain(&self) -> &[CapturedX509Certificate] {
&self.certificates
}
pub fn chain_apple_certificates(&mut self) -> Option<Vec<CapturedX509Certificate>> {
if let Some((_, cert)) = &self.signing_key {
if let Some(chain) = cert.apple_root_certificate_chain() {
let chain = chain.into_iter().skip(1).collect::<Vec<_>>();
self.certificates.extend(chain.clone());
Some(chain)
} else {
None
}
} else {
None
}
}
pub fn chain_certificate(&mut self, cert: CapturedX509Certificate) {
self.certificates.push(cert);
}
pub fn chain_certificate_der(
&mut self,
data: impl AsRef<[u8]>,
) -> Result<(), AppleCodesignError> {
self.chain_certificate(CapturedX509Certificate::from_der(data.as_ref())?);
Ok(())
}
pub fn chain_certificate_pem(
&mut self,
data: impl AsRef<[u8]>,
) -> Result<(), AppleCodesignError> {
self.chain_certificate(CapturedX509Certificate::from_pem(data.as_ref())?);
Ok(())
}
pub fn time_stamp_url(&self) -> Option<&Url> {
self.time_stamp_url.as_ref()
}
pub fn set_time_stamp_url(&mut self, url: impl IntoUrl) -> Result<(), AppleCodesignError> {
self.time_stamp_url = Some(url.into_url()?);
Ok(())
}
pub fn team_id(&self) -> Option<&str> {
self.team_id.as_deref()
}
pub fn set_team_id(&mut self, value: impl ToString) {
self.team_id = Some(value.to_string());
}
pub fn binary_identifier(&self, scope: impl AsRef<SettingsScope>) -> Option<&str> {
self.identifiers.get(scope.as_ref()).map(|s| s.as_str())
}
pub fn set_binary_identifier(&mut self, scope: SettingsScope, value: impl ToString) {
self.identifiers.insert(scope, value.to_string());
}
pub fn entitlements_xml(&self, scope: impl AsRef<SettingsScope>) -> Option<&str> {
self.entitlements.get(scope.as_ref()).map(|s| s.as_str())
}
pub fn set_entitlements_xml(&mut self, scope: SettingsScope, value: impl ToString) {
self.entitlements.insert(scope, value.to_string());
}
pub fn designated_requirement(
&self,
scope: impl AsRef<SettingsScope>,
) -> &DesignatedRequirementMode {
self.designated_requirement
.get(scope.as_ref())
.unwrap_or(&DesignatedRequirementMode::Auto)
}
pub fn set_designated_requirement_expression(
&mut self,
scope: SettingsScope,
expr: &CodeRequirementExpression,
) -> Result<(), AppleCodesignError> {
self.designated_requirement.insert(
scope,
DesignatedRequirementMode::Explicit(vec![expr.to_bytes()?]),
);
Ok(())
}
pub fn set_designated_requirement_bytes(
&mut self,
scope: SettingsScope,
data: impl AsRef<[u8]>,
) -> Result<(), AppleCodesignError> {
let blob = RequirementBlob::from_blob_bytes(data.as_ref())?;
self.designated_requirement.insert(
scope,
DesignatedRequirementMode::Explicit(
blob.parse_expressions()?
.iter()
.map(|x| x.to_bytes())
.collect::<Result<Vec<_>, AppleCodesignError>>()?,
),
);
Ok(())
}
pub fn set_auto_designated_requirement(&mut self, scope: SettingsScope) {
self.designated_requirement
.insert(scope, DesignatedRequirementMode::Auto);
}
pub fn code_signature_flags(
&self,
scope: impl AsRef<SettingsScope>,
) -> Option<CodeSignatureFlags> {
self.code_signature_flags.get(scope.as_ref()).copied()
}
pub fn set_code_signature_flags(&mut self, scope: SettingsScope, flags: CodeSignatureFlags) {
self.code_signature_flags.insert(scope, flags);
}
pub fn add_code_signature_flags(
&mut self,
scope: SettingsScope,
flags: CodeSignatureFlags,
) -> CodeSignatureFlags {
let existing = self
.code_signature_flags
.get(&scope)
.copied()
.unwrap_or_else(CodeSignatureFlags::empty);
let new = existing | flags;
self.code_signature_flags.insert(scope, new);
new
}
pub fn remove_code_signature_flags(
&mut self,
scope: SettingsScope,
flags: CodeSignatureFlags,
) -> CodeSignatureFlags {
let existing = self
.code_signature_flags
.get(&scope)
.copied()
.unwrap_or_else(CodeSignatureFlags::empty);
let new = existing - flags;
self.code_signature_flags.insert(scope, new);
new
}
pub fn executable_segment_flags(
&self,
scope: impl AsRef<SettingsScope>,
) -> Option<ExecutableSegmentFlags> {
self.executable_segment_flags.get(scope.as_ref()).copied()
}
pub fn set_executable_segment_flags(
&mut self,
scope: SettingsScope,
flags: ExecutableSegmentFlags,
) {
self.executable_segment_flags.insert(scope, flags);
}
pub fn info_plist_data(&self, scope: impl AsRef<SettingsScope>) -> Option<&[u8]> {
self.info_plist_data
.get(scope.as_ref())
.map(|x| x.as_slice())
}
pub fn set_info_plist_data(&mut self, scope: SettingsScope, data: Vec<u8>) {
self.info_plist_data.insert(scope, data);
}
pub fn code_resources_data(&self, scope: impl AsRef<SettingsScope>) -> Option<&[u8]> {
self.code_resources_data
.get(scope.as_ref())
.map(|x| x.as_slice())
}
pub fn set_code_resources_data(&mut self, scope: SettingsScope, data: Vec<u8>) {
self.code_resources_data.insert(scope, data);
}
pub fn as_nested_bundle_settings(&self, bundle_path: &str) -> Self {
self.clone_strip_prefix(bundle_path, format!("{}/", bundle_path))
}
pub fn as_bundle_macho_settings(&self, path: &str) -> Self {
self.clone_strip_prefix(path, path.to_string())
}
pub fn as_nested_macho_settings(&self, index: usize, cpu_type: CpuType) -> Self {
self.clone_with_filter_map(|key| {
if key == SettingsScope::Main
|| key == SettingsScope::MultiArchCpuType(cpu_type)
|| key == SettingsScope::MultiArchIndex(index)
{
Some(SettingsScope::Main)
} else {
None
}
})
}
fn clone_strip_prefix(&self, main_path: &str, prefix: String) -> Self {
self.clone_with_filter_map(|key| match key {
SettingsScope::Main => Some(SettingsScope::Main),
SettingsScope::Path(path) => {
if path == main_path {
Some(SettingsScope::Main)
} else {
path.strip_prefix(&prefix)
.map(|path| SettingsScope::Path(path.to_string()))
}
}
SettingsScope::MultiArchIndex(index) => Some(SettingsScope::MultiArchIndex(index)),
SettingsScope::MultiArchCpuType(cpu_type) => {
Some(SettingsScope::MultiArchCpuType(cpu_type))
}
SettingsScope::PathMultiArchIndex(path, index) => {
if path == main_path {
Some(SettingsScope::MultiArchIndex(index))
} else {
path.strip_prefix(&prefix)
.map(|path| SettingsScope::PathMultiArchIndex(path.to_string(), index))
}
}
SettingsScope::PathMultiArchCpuType(path, cpu_type) => {
if path == main_path {
Some(SettingsScope::MultiArchCpuType(cpu_type))
} else {
path.strip_prefix(&prefix)
.map(|path| SettingsScope::PathMultiArchCpuType(path.to_string(), cpu_type))
}
}
})
}
fn clone_with_filter_map(
&self,
key_map: impl Fn(SettingsScope) -> Option<SettingsScope>,
) -> Self {
Self {
signing_key: self.signing_key.clone(),
certificates: self.certificates.clone(),
time_stamp_url: self.time_stamp_url.clone(),
team_id: self.team_id.clone(),
digest_type: self.digest_type,
identifiers: self
.identifiers
.clone()
.into_iter()
.filter_map(|(key, value)| key_map(key).map(|key| (key, value)))
.collect::<BTreeMap<_, _>>(),
entitlements: self
.entitlements
.clone()
.into_iter()
.filter_map(|(key, value)| key_map(key).map(|key| (key, value)))
.collect::<BTreeMap<_, _>>(),
designated_requirement: self
.designated_requirement
.clone()
.into_iter()
.filter_map(|(key, value)| key_map(key).map(|key| (key, value)))
.collect::<BTreeMap<_, _>>(),
code_signature_flags: self
.code_signature_flags
.clone()
.into_iter()
.filter_map(|(key, value)| key_map(key).map(|key| (key, value)))
.collect::<BTreeMap<_, _>>(),
executable_segment_flags: self
.executable_segment_flags
.clone()
.into_iter()
.filter_map(|(key, value)| key_map(key).map(|key| (key, value)))
.collect::<BTreeMap<_, _>>(),
info_plist_data: self
.info_plist_data
.clone()
.into_iter()
.filter_map(|(key, value)| key_map(key).map(|key| (key, value)))
.collect::<BTreeMap<_, _>>(),
code_resources_data: self
.code_resources_data
.clone()
.into_iter()
.filter_map(|(key, value)| key_map(key).map(|key| (key, value)))
.collect::<BTreeMap<_, _>>(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_settings_scope() {
assert_eq!(
SettingsScope::try_from("@main").unwrap(),
SettingsScope::Main
);
assert_eq!(
SettingsScope::try_from("@0").unwrap(),
SettingsScope::MultiArchIndex(0)
);
assert_eq!(
SettingsScope::try_from("@42").unwrap(),
SettingsScope::MultiArchIndex(42)
);
assert_eq!(
SettingsScope::try_from("@[cpu_type=7]").unwrap(),
SettingsScope::MultiArchCpuType(7)
);
assert_eq!(
SettingsScope::try_from("@[cpu_type=arm]").unwrap(),
SettingsScope::MultiArchCpuType(CPU_TYPE_ARM)
);
assert_eq!(
SettingsScope::try_from("@[cpu_type=arm64]").unwrap(),
SettingsScope::MultiArchCpuType(CPU_TYPE_ARM64)
);
assert_eq!(
SettingsScope::try_from("@[cpu_type=arm64_32]").unwrap(),
SettingsScope::MultiArchCpuType(CPU_TYPE_ARM64_32)
);
assert_eq!(
SettingsScope::try_from("@[cpu_type=x86_64]").unwrap(),
SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64)
);
assert_eq!(
SettingsScope::try_from("foo/bar").unwrap(),
SettingsScope::Path("foo/bar".into())
);
assert_eq!(
SettingsScope::try_from("foo/bar@0").unwrap(),
SettingsScope::PathMultiArchIndex("foo/bar".into(), 0)
);
assert_eq!(
SettingsScope::try_from("foo/bar@[cpu_type=7]").unwrap(),
SettingsScope::PathMultiArchCpuType("foo/bar".into(), 7_u32)
);
}
#[test]
fn as_nested_macho_settings() {
let mut main_settings = SigningSettings::default();
main_settings.set_binary_identifier(SettingsScope::Main, "ident");
main_settings
.set_code_signature_flags(SettingsScope::Main, CodeSignatureFlags::FORCE_EXPIRATION);
main_settings.set_code_signature_flags(
SettingsScope::MultiArchIndex(0),
CodeSignatureFlags::FORCE_HARD,
);
main_settings.set_code_signature_flags(
SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64),
CodeSignatureFlags::RESTRICT,
);
main_settings.set_entitlements_xml(SettingsScope::MultiArchIndex(0), "index_0");
main_settings.set_entitlements_xml(
SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64),
"cpu_x86_64",
);
let macho_settings = main_settings.as_nested_macho_settings(0, CPU_TYPE_ARM64);
assert_eq!(
macho_settings.binary_identifier(SettingsScope::Main),
Some("ident")
);
assert_eq!(
macho_settings.code_signature_flags(SettingsScope::Main),
Some(CodeSignatureFlags::FORCE_HARD)
);
assert_eq!(
macho_settings.entitlements_xml(SettingsScope::Main),
Some("index_0")
);
let macho_settings = main_settings.as_nested_macho_settings(0, CPU_TYPE_X86_64);
assert_eq!(
macho_settings.binary_identifier(SettingsScope::Main),
Some("ident")
);
assert_eq!(
macho_settings.code_signature_flags(SettingsScope::Main),
Some(CodeSignatureFlags::RESTRICT)
);
assert_eq!(
macho_settings.entitlements_xml(SettingsScope::Main),
Some("cpu_x86_64")
);
}
#[test]
fn as_bundle_macho_settings() {
let mut main_settings = SigningSettings::default();
main_settings.set_entitlements_xml(SettingsScope::Main, "main");
main_settings.set_entitlements_xml(
SettingsScope::Path("Contents/MacOS/main".into()),
"main_exe",
);
main_settings.set_entitlements_xml(
SettingsScope::PathMultiArchIndex("Contents/MacOS/main".into(), 0),
"main_exe_index_0",
);
main_settings.set_entitlements_xml(
SettingsScope::PathMultiArchCpuType("Contents/MacOS/main".into(), CPU_TYPE_X86_64),
"main_exe_x86_64",
);
let macho_settings = main_settings.as_bundle_macho_settings("Contents/MacOS/main");
assert_eq!(
macho_settings.entitlements_xml(SettingsScope::Main),
Some("main_exe")
);
assert_eq!(
macho_settings.entitlements,
[
(SettingsScope::Main, "main_exe".into()),
(SettingsScope::MultiArchIndex(0), "main_exe_index_0".into()),
(
SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64),
"main_exe_x86_64".into()
),
]
.iter()
.cloned()
.collect::<BTreeMap<SettingsScope, String>>()
);
}
#[test]
fn as_nested_bundle_settings() {
let mut main_settings = SigningSettings::default();
main_settings.set_entitlements_xml(SettingsScope::Main, "main");
main_settings.set_entitlements_xml(
SettingsScope::Path("Contents/MacOS/main".into()),
"main_exe",
);
main_settings.set_entitlements_xml(
SettingsScope::Path("Contents/MacOS/nested.app".into()),
"bundle",
);
main_settings.set_entitlements_xml(
SettingsScope::PathMultiArchIndex("Contents/MacOS/nested.app".into(), 0),
"bundle_index_0",
);
main_settings.set_entitlements_xml(
SettingsScope::PathMultiArchCpuType(
"Contents/MacOS/nested.app".into(),
CPU_TYPE_X86_64,
),
"bundle_x86_64",
);
main_settings.set_entitlements_xml(
SettingsScope::Path("Contents/MacOS/nested.app/Contents/MacOS/nested".into()),
"nested_main_exe",
);
main_settings.set_entitlements_xml(
SettingsScope::PathMultiArchIndex(
"Contents/MacOS/nested.app/Contents/MacOS/nested".into(),
0,
),
"nested_main_exe_index_0",
);
main_settings.set_entitlements_xml(
SettingsScope::PathMultiArchCpuType(
"Contents/MacOS/nested.app/Contents/MacOS/nested".into(),
CPU_TYPE_X86_64,
),
"nested_main_exe_x86_64",
);
let bundle_settings = main_settings.as_nested_bundle_settings("Contents/MacOS/nested.app");
assert_eq!(
bundle_settings.entitlements_xml(SettingsScope::Main),
Some("bundle")
);
assert_eq!(
bundle_settings.entitlements_xml(SettingsScope::Path("Contents/MacOS/nested".into())),
Some("nested_main_exe")
);
assert_eq!(
bundle_settings.entitlements,
[
(SettingsScope::Main, "bundle".into()),
(SettingsScope::MultiArchIndex(0), "bundle_index_0".into()),
(
SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64),
"bundle_x86_64".into()
),
(
SettingsScope::Path("Contents/MacOS/nested".into()),
"nested_main_exe".into()
),
(
SettingsScope::PathMultiArchIndex("Contents/MacOS/nested".into(), 0),
"nested_main_exe_index_0".into()
),
(
SettingsScope::PathMultiArchCpuType(
"Contents/MacOS/nested".into(),
CPU_TYPE_X86_64
),
"nested_main_exe_x86_64".into()
),
]
.iter()
.cloned()
.collect::<BTreeMap<SettingsScope, String>>()
);
}
}