verifyos_cli/rules/
signing.rs1use crate::parsers::plist_reader::InfoPlist;
2use crate::rules::core::{
3 AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
4};
5
6pub struct EmbeddedCodeSignatureTeamRule;
7
8impl AppStoreRule for EmbeddedCodeSignatureTeamRule {
9 fn id(&self) -> &'static str {
10 "RULE_EMBEDDED_TEAM_ID_MISMATCH"
11 }
12
13 fn name(&self) -> &'static str {
14 "Embedded Team ID Mismatch"
15 }
16
17 fn category(&self) -> RuleCategory {
18 RuleCategory::Signing
19 }
20
21 fn severity(&self) -> Severity {
22 Severity::Error
23 }
24
25 fn recommendation(&self) -> &'static str {
26 "Ensure all embedded frameworks/extensions are signed with the same Team ID as the app binary."
27 }
28
29 fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
30 let info_plist = match artifact.info_plist {
31 Some(plist) => plist,
32 None => match artifact.bundle_info_plist(artifact.app_bundle_path) {
33 Ok(Some(plist)) => return evaluate_with_plist(artifact, &plist),
34 Ok(None) => {
35 return Ok(RuleReport {
36 status: RuleStatus::Skip,
37 message: Some("Info.plist not found".to_string()),
38 evidence: None,
39 })
40 }
41 Err(err) => {
42 let plist_path = artifact.app_bundle_path.join("Info.plist");
43 return Ok(RuleReport {
44 status: RuleStatus::Skip,
45 message: Some(format!("Failed to parse Info.plist: {err}")),
46 evidence: Some(plist_path.display().to_string()),
47 });
48 }
49 },
50 };
51
52 evaluate_with_plist(artifact, info_plist)
53 }
54}
55
56fn evaluate_with_plist(
57 artifact: &ArtifactContext,
58 info_plist: &InfoPlist,
59) -> Result<RuleReport, RuleError> {
60 let Some(app_executable) = info_plist.get_string("CFBundleExecutable") else {
61 return Ok(RuleReport {
62 status: RuleStatus::Skip,
63 message: Some("CFBundleExecutable not found".to_string()),
64 evidence: None,
65 });
66 };
67
68 let app_executable_path = artifact.app_bundle_path.join(app_executable);
69 if !app_executable_path.exists() {
70 return Ok(RuleReport {
71 status: RuleStatus::Skip,
72 message: Some("App executable not found".to_string()),
73 evidence: Some(app_executable_path.display().to_string()),
74 });
75 }
76
77 let app_summary = artifact
78 .signature_summary(&app_executable_path)
79 .map_err(RuleError::MachO)?;
80
81 if app_summary.total_slices == 0 {
82 return Ok(RuleReport {
83 status: RuleStatus::Skip,
84 message: Some("No Mach-O slices found".to_string()),
85 evidence: Some(app_executable_path.display().to_string()),
86 });
87 }
88
89 if app_summary.signed_slices == 0 {
90 return Ok(RuleReport {
91 status: RuleStatus::Fail,
92 message: Some("App executable missing code signature".to_string()),
93 evidence: Some(app_executable_path.display().to_string()),
94 });
95 }
96
97 if app_summary.signed_slices < app_summary.total_slices {
98 return Ok(RuleReport {
99 status: RuleStatus::Fail,
100 message: Some("App executable has unsigned slices".to_string()),
101 evidence: Some(app_executable_path.display().to_string()),
102 });
103 }
104
105 let Some(app_team_id) = app_summary.team_id else {
106 return Ok(RuleReport {
107 status: RuleStatus::Fail,
108 message: Some("App executable missing Team ID".to_string()),
109 evidence: Some(app_executable_path.display().to_string()),
110 });
111 };
112
113 let bundles = artifact
114 .nested_bundles()
115 .map_err(|_| crate::rules::entitlements::EntitlementsError::ParseFailure)?;
116
117 if bundles.is_empty() {
118 return Ok(RuleReport {
119 status: RuleStatus::Pass,
120 message: Some("No embedded bundles found".to_string()),
121 evidence: None,
122 });
123 }
124
125 let mut mismatches = Vec::new();
126
127 for bundle in bundles {
128 let Some(executable_path) = artifact.executable_path_for_bundle(&bundle.bundle_path) else {
129 mismatches.push(format!(
130 "{}: Missing CFBundleExecutable",
131 bundle.display_name
132 ));
133 continue;
134 };
135
136 if !executable_path.exists() {
137 mismatches.push(format!(
138 "{}: Executable not found at {}",
139 bundle.display_name,
140 executable_path.display()
141 ));
142 continue;
143 }
144
145 let summary = artifact
146 .signature_summary(&executable_path)
147 .map_err(RuleError::MachO)?;
148
149 if summary.total_slices == 0 {
150 mismatches.push(format!("{}: No Mach-O slices found", bundle.display_name));
151 continue;
152 }
153
154 if summary.signed_slices == 0 {
155 mismatches.push(format!("{}: Missing code signature", bundle.display_name));
156 continue;
157 }
158
159 if summary.signed_slices < summary.total_slices {
160 mismatches.push(format!("{}: Unsigned Mach-O slices", bundle.display_name));
161 continue;
162 }
163
164 let Some(team_id) = summary.team_id else {
165 mismatches.push(format!("{}: Missing Team ID", bundle.display_name));
166 continue;
167 };
168
169 if team_id != app_team_id {
170 mismatches.push(format!(
171 "{}: Team ID mismatch ({} != {})",
172 bundle.display_name, team_id, app_team_id
173 ));
174 }
175 }
176
177 if mismatches.is_empty() {
178 return Ok(RuleReport {
179 status: RuleStatus::Pass,
180 message: Some("Embedded bundles share the same Team ID".to_string()),
181 evidence: None,
182 });
183 }
184
185 Ok(RuleReport {
186 status: RuleStatus::Fail,
187 message: Some("Embedded bundle signing mismatch".to_string()),
188 evidence: Some(mismatches.join(" | ")),
189 })
190}