1use {
19 crate::{
20 bundle_signing::{BundleSigningContext, SignedMachOInfo},
21 cryptography::{DigestType, MultiDigest},
22 error::AppleCodesignError,
23 },
24 apple_bundles::DirectoryBundle,
25 log::{debug, error, info, warn},
26 plist::{Dictionary, Value},
27 std::{
28 cmp::Ordering,
29 collections::{BTreeMap, BTreeSet},
30 io::Write,
31 path::Path,
32 },
33};
34
35#[derive(Clone, PartialEq)]
36enum FilesValue {
37 Required(Vec<u8>),
38 Optional(Vec<u8>),
39}
40
41impl std::fmt::Debug for FilesValue {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 Self::Required(digest) => f
45 .debug_struct("FilesValue")
46 .field("required", &true)
47 .field("digest", &hex::encode(digest))
48 .finish(),
49 Self::Optional(digest) => f
50 .debug_struct("FilesValue")
51 .field("required", &false)
52 .field("digest", &hex::encode(digest))
53 .finish(),
54 }
55 }
56}
57
58impl std::fmt::Display for FilesValue {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 match self {
61 Self::Required(digest) => {
62 f.write_fmt(format_args!("{} (required)", hex::encode(digest)))
63 }
64 Self::Optional(digest) => {
65 f.write_fmt(format_args!("{} (optional)", hex::encode(digest)))
66 }
67 }
68 }
69}
70
71impl TryFrom<&Value> for FilesValue {
72 type Error = AppleCodesignError;
73
74 fn try_from(v: &Value) -> Result<Self, Self::Error> {
75 match v {
76 Value::Data(digest) => Ok(Self::Required(digest.to_vec())),
77 Value::Dictionary(dict) => {
78 let mut digest = None;
79 let mut optional = None;
80
81 for (key, value) in dict.iter() {
82 match key.as_str() {
83 "hash" => {
84 let data = value.as_data().ok_or_else(|| {
85 AppleCodesignError::ResourcesPlistParse(format!(
86 "expected <data> for files <dict> entry, got {value:?}"
87 ))
88 })?;
89
90 digest = Some(data.to_vec());
91 }
92 "optional" => {
93 let v = value.as_boolean().ok_or_else(|| {
94 AppleCodesignError::ResourcesPlistParse(format!(
95 "expected boolean for optional key, got {value:?}"
96 ))
97 })?;
98
99 optional = Some(v);
100 }
101 key => {
102 return Err(AppleCodesignError::ResourcesPlistParse(format!(
103 "unexpected key in files dict: {key}"
104 )));
105 }
106 }
107 }
108
109 match (digest, optional) {
110 (Some(digest), Some(true)) => Ok(Self::Optional(digest)),
111 (Some(digest), Some(false)) => Ok(Self::Required(digest)),
112 _ => Err(AppleCodesignError::ResourcesPlistParse(
113 "missing hash or optional key".to_string(),
114 )),
115 }
116 }
117 _ => Err(AppleCodesignError::ResourcesPlistParse(format!(
118 "bad value in files <dict>; expected <data> or <dict>, got {v:?}"
119 ))),
120 }
121 }
122}
123
124impl From<&FilesValue> for Value {
125 fn from(v: &FilesValue) -> Self {
126 match v {
127 FilesValue::Required(digest) => Self::Data(digest.to_vec()),
128 FilesValue::Optional(digest) => {
129 let mut dict = Dictionary::new();
130 dict.insert("hash".to_string(), Value::Data(digest.to_vec()));
131 dict.insert("optional".to_string(), Value::Boolean(true));
132
133 Self::Dictionary(dict)
134 }
135 }
136 }
137}
138
139#[derive(Clone, PartialEq)]
140struct Files2Value {
141 cdhash: Option<Vec<u8>>,
142 hash: Option<Vec<u8>>,
143 hash2: Option<Vec<u8>>,
144 optional: Option<bool>,
145 requirement: Option<String>,
146 symlink: Option<String>,
147}
148
149impl std::fmt::Debug for Files2Value {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 f.debug_struct("Files2Value")
152 .field(
153 "cdhash",
154 &format_args!("{:?}", self.cdhash.as_ref().map(hex::encode)),
155 )
156 .field(
157 "hash",
158 &format_args!("{:?}", self.hash.as_ref().map(hex::encode)),
159 )
160 .field(
161 "hash2",
162 &format_args!("{:?}", self.hash2.as_ref().map(hex::encode)),
163 )
164 .field("optional", &format_args!("{:?}", self.optional))
165 .field("requirement", &format_args!("{:?}", self.requirement))
166 .field("symlink", &format_args!("{:?}", self.symlink))
167 .finish()
168 }
169}
170
171impl TryFrom<&Value> for Files2Value {
172 type Error = AppleCodesignError;
173
174 fn try_from(v: &Value) -> Result<Self, Self::Error> {
175 let dict = v.as_dictionary().ok_or_else(|| {
176 AppleCodesignError::ResourcesPlistParse("files2 value should be a dict".to_string())
177 })?;
178
179 let mut hash = None;
180 let mut hash2 = None;
181 let mut cdhash = None;
182 let mut optional = None;
183 let mut requirement = None;
184 let mut symlink = None;
185
186 for (key, value) in dict.iter() {
187 match key.as_str() {
188 "cdhash" => {
189 let data = value.as_data().ok_or_else(|| {
190 AppleCodesignError::ResourcesPlistParse(format!(
191 "expected <data> for files2 cdhash entry, got {value:?}"
192 ))
193 })?;
194
195 cdhash = Some(data.to_vec());
196 }
197 "hash" => {
198 let data = value.as_data().ok_or_else(|| {
199 AppleCodesignError::ResourcesPlistParse(format!(
200 "expected <data> for files2 hash entry, got {value:?}"
201 ))
202 })?;
203
204 hash = Some(data.to_vec());
205 }
206 "hash2" => {
207 let data = value.as_data().ok_or_else(|| {
208 AppleCodesignError::ResourcesPlistParse(format!(
209 "expected <data> for files2 hash2 entry, got {value:?}"
210 ))
211 })?;
212
213 hash2 = Some(data.to_vec());
214 }
215 "optional" => {
216 let v = value.as_boolean().ok_or_else(|| {
217 AppleCodesignError::ResourcesPlistParse(format!(
218 "expected bool for optional key, got {value:?}"
219 ))
220 })?;
221
222 optional = Some(v);
223 }
224 "requirement" => {
225 let v = value.as_string().ok_or_else(|| {
226 AppleCodesignError::ResourcesPlistParse(format!(
227 "expected string for requirement key, got {value:?}"
228 ))
229 })?;
230
231 requirement = Some(v.to_string());
232 }
233 "symlink" => {
234 symlink = Some(
235 value
236 .as_string()
237 .ok_or_else(|| {
238 AppleCodesignError::ResourcesPlistParse(format!(
239 "expected string for symlink key, got {value:?}"
240 ))
241 })?
242 .to_string(),
243 );
244 }
245 key => {
246 return Err(AppleCodesignError::ResourcesPlistParse(format!(
247 "unexpected key in files2 dict entry: {key}"
248 )));
249 }
250 }
251 }
252
253 Ok(Self {
254 cdhash,
255 hash,
256 hash2,
257 optional,
258 requirement,
259 symlink,
260 })
261 }
262}
263
264impl From<&Files2Value> for Value {
265 fn from(v: &Files2Value) -> Self {
266 let mut dict = Dictionary::new();
267
268 if let Some(cdhash) = &v.cdhash {
269 dict.insert("cdhash".to_string(), Value::Data(cdhash.to_vec()));
270 }
271
272 if let Some(hash) = &v.hash {
273 dict.insert("hash".to_string(), Value::Data(hash.to_vec()));
274 }
275
276 if let Some(hash2) = &v.hash2 {
277 dict.insert("hash2".to_string(), Value::Data(hash2.to_vec()));
278 }
279
280 if let Some(optional) = &v.optional {
281 dict.insert("optional".to_string(), Value::Boolean(*optional));
282 }
283
284 if let Some(requirement) = &v.requirement {
285 dict.insert(
286 "requirement".to_string(),
287 Value::String(requirement.to_string()),
288 );
289 }
290
291 if let Some(symlink) = &v.symlink {
292 dict.insert("symlink".to_string(), Value::String(symlink.to_string()));
293 }
294
295 Value::Dictionary(dict)
296 }
297}
298
299#[derive(Clone, Debug, PartialEq)]
300struct RulesValue {
301 omit: bool,
302 required: bool,
303 weight: Option<f64>,
304}
305
306impl TryFrom<&Value> for RulesValue {
307 type Error = AppleCodesignError;
308
309 fn try_from(v: &Value) -> Result<Self, Self::Error> {
310 match v {
311 Value::Boolean(true) => Ok(Self {
312 omit: false,
313 required: true,
314 weight: None,
315 }),
316 Value::Dictionary(dict) => {
317 let mut omit = None;
318 let mut optional = None;
319 let mut weight = None;
320
321 for (key, value) in dict {
322 match key.as_str() {
323 "omit" => {
324 omit = Some(value.as_boolean().ok_or_else(|| {
325 AppleCodesignError::ResourcesPlistParse(format!(
326 "rules omit key value not a boolean; got {value:?}"
327 ))
328 })?);
329 }
330 "optional" => {
331 optional = Some(value.as_boolean().ok_or_else(|| {
332 AppleCodesignError::ResourcesPlistParse(format!(
333 "rules optional key value not a boolean, got {value:?}"
334 ))
335 })?);
336 }
337 "weight" => {
338 weight = Some(value.as_real().ok_or_else(|| {
339 AppleCodesignError::ResourcesPlistParse(format!(
340 "rules weight key value not a real, got {value:?}"
341 ))
342 })?);
343 }
344 key => {
345 return Err(AppleCodesignError::ResourcesPlistParse(format!(
346 "extra key in rules dict: {key}"
347 )));
348 }
349 }
350 }
351
352 Ok(Self {
353 omit: omit.unwrap_or(false),
354 required: !optional.unwrap_or(false),
355 weight,
356 })
357 }
358 _ => Err(AppleCodesignError::ResourcesPlistParse(
359 "invalid value for rules entry".to_string(),
360 )),
361 }
362 }
363}
364
365impl From<&RulesValue> for Value {
366 fn from(v: &RulesValue) -> Self {
367 if v.required && !v.omit && v.weight.is_none() {
368 Value::Boolean(true)
369 } else {
370 let mut dict = Dictionary::new();
371
372 if v.omit {
373 dict.insert("omit".to_string(), Value::Boolean(true));
374 }
375 if !v.required {
376 dict.insert("optional".to_string(), Value::Boolean(true));
377 }
378
379 if let Some(weight) = v.weight {
380 dict.insert("weight".to_string(), Value::Real(weight));
381 }
382
383 Value::Dictionary(dict)
384 }
385 }
386}
387
388#[derive(Clone, Debug, PartialEq)]
389struct Rules2Value {
390 nested: Option<bool>,
391 omit: Option<bool>,
392 optional: Option<bool>,
393 weight: Option<f64>,
394}
395
396impl TryFrom<&Value> for Rules2Value {
397 type Error = AppleCodesignError;
398
399 fn try_from(v: &Value) -> Result<Self, Self::Error> {
400 let dict = v.as_dictionary().ok_or_else(|| {
401 AppleCodesignError::ResourcesPlistParse("rules2 value should be a dict".to_string())
402 })?;
403
404 let mut nested = None;
405 let mut omit = None;
406 let mut optional = None;
407 let mut weight = None;
408
409 for (key, value) in dict.iter() {
410 match key.as_str() {
411 "nested" => {
412 nested = Some(value.as_boolean().ok_or_else(|| {
413 AppleCodesignError::ResourcesPlistParse(format!(
414 "expected bool for rules2 nested key, got {value:?}"
415 ))
416 })?);
417 }
418 "omit" => {
419 omit = Some(value.as_boolean().ok_or_else(|| {
420 AppleCodesignError::ResourcesPlistParse(format!(
421 "expected bool for rules2 omit key, got {value:?}"
422 ))
423 })?);
424 }
425 "optional" => {
426 optional = Some(value.as_boolean().ok_or_else(|| {
427 AppleCodesignError::ResourcesPlistParse(format!(
428 "expected bool for rules2 optional key, got {value:?}"
429 ))
430 })?);
431 }
432 "weight" => {
433 weight = Some(value.as_real().ok_or_else(|| {
434 AppleCodesignError::ResourcesPlistParse(format!(
435 "expected real for rules2 weight key, got {value:?}"
436 ))
437 })?);
438 }
439 key => {
440 return Err(AppleCodesignError::ResourcesPlistParse(format!(
441 "unexpected key in rules dict entry: {key}"
442 )));
443 }
444 }
445 }
446
447 Ok(Self {
448 nested,
449 omit,
450 optional,
451 weight,
452 })
453 }
454}
455
456impl From<&Rules2Value> for Value {
457 fn from(v: &Rules2Value) -> Self {
458 let mut dict = Dictionary::new();
459
460 if let Some(true) = v.nested {
461 dict.insert("nested".to_string(), Value::Boolean(true));
462 }
463
464 if let Some(true) = v.omit {
465 dict.insert("omit".to_string(), Value::Boolean(true));
466 }
467
468 if let Some(true) = v.optional {
469 dict.insert("optional".to_string(), Value::Boolean(true));
470 }
471
472 if let Some(weight) = v.weight {
473 dict.insert("weight".to_string(), Value::Real(weight));
474 }
475
476 if dict.is_empty() {
477 Value::Boolean(true)
478 } else {
479 Value::Dictionary(dict)
480 }
481 }
482}
483
484#[derive(Clone, Debug)]
489pub struct CodeResourcesRule {
490 pub pattern: String,
494
495 pub exclude: bool,
499
500 pub nested: bool,
505
506 pub omit: bool,
511
512 pub optional: bool,
514
515 pub weight: Option<u32>,
517
518 re: regex::Regex,
519}
520
521impl PartialEq for CodeResourcesRule {
522 fn eq(&self, other: &Self) -> bool {
523 self.pattern == other.pattern
524 && self.exclude == other.exclude
525 && self.nested == other.nested
526 && self.omit == other.omit
527 && self.optional == other.optional
528 && self.weight == other.weight
529 }
530}
531
532impl Eq for CodeResourcesRule {}
533
534impl PartialOrd for CodeResourcesRule {
535 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
536 Some(self.cmp(other))
537 }
538}
539
540impl Ord for CodeResourcesRule {
541 fn cmp(&self, other: &Self) -> Ordering {
542 let our_weight = self.weight.unwrap_or(1);
544 let their_weight = other.weight.unwrap_or(1);
545
546 match (self.exclude, other.exclude) {
549 (true, false) => Ordering::Less,
550 (false, true) => Ordering::Greater,
551 _ => their_weight.cmp(&our_weight),
552 }
553 }
554}
555
556impl CodeResourcesRule {
557 pub fn new(pattern: impl ToString) -> Result<Self, AppleCodesignError> {
558 Ok(Self {
559 pattern: pattern.to_string(),
560 exclude: false,
561 nested: false,
562 omit: false,
563 optional: false,
564 weight: None,
565 re: regex::Regex::new(&pattern.to_string())
566 .map_err(|e| AppleCodesignError::ResourcesBadRegex(pattern.to_string(), e))?,
567 })
568 }
569
570 #[must_use]
575 pub fn exclude(mut self) -> Self {
576 self.exclude = true;
577 self
578 }
579
580 #[must_use]
582 pub fn nested(mut self) -> Self {
583 self.nested = true;
584 self
585 }
586
587 #[must_use]
589 pub fn omit(mut self) -> Self {
590 self.omit = true;
591 self
592 }
593
594 #[must_use]
596 pub fn optional(mut self) -> Self {
597 self.optional = true;
598 self
599 }
600
601 #[must_use]
603 pub fn weight(mut self, v: u32) -> Self {
604 self.weight = Some(v);
605 self
606 }
607}
608
609#[derive(Clone, Copy, Debug)]
611pub enum FilesFlavor {
612 Rules,
614 Rules2,
616 Rules2WithSha1,
618}
619
620#[derive(Clone, Debug, Default, PartialEq)]
625pub struct CodeResources {
626 files: BTreeMap<String, FilesValue>,
627 files2: BTreeMap<String, Files2Value>,
628 rules: BTreeMap<String, RulesValue>,
629 rules2: BTreeMap<String, Rules2Value>,
630}
631
632impl CodeResources {
633 pub fn from_xml(xml: &[u8]) -> Result<Self, AppleCodesignError> {
635 let plist = Value::from_reader_xml(xml).map_err(AppleCodesignError::ResourcesPlist)?;
636
637 let dict = plist.into_dictionary().ok_or_else(|| {
638 AppleCodesignError::ResourcesPlistParse(
639 "plist root element should be a <dict>".to_string(),
640 )
641 })?;
642
643 let mut files = BTreeMap::new();
644 let mut files2 = BTreeMap::new();
645 let mut rules = BTreeMap::new();
646 let mut rules2 = BTreeMap::new();
647
648 for (key, value) in dict.iter() {
649 match key.as_ref() {
650 "files" => {
651 let dict = value.as_dictionary().ok_or_else(|| {
652 AppleCodesignError::ResourcesPlistParse(format!(
653 "expecting files to be a dict, got {value:?}"
654 ))
655 })?;
656
657 for (key, value) in dict {
658 files.insert(key.to_string(), FilesValue::try_from(value)?);
659 }
660 }
661 "files2" => {
662 let dict = value.as_dictionary().ok_or_else(|| {
663 AppleCodesignError::ResourcesPlistParse(format!(
664 "expecting files2 to be a dict, got {value:?}"
665 ))
666 })?;
667
668 for (key, value) in dict {
669 files2.insert(key.to_string(), Files2Value::try_from(value)?);
670 }
671 }
672 "rules" => {
673 let dict = value.as_dictionary().ok_or_else(|| {
674 AppleCodesignError::ResourcesPlistParse(format!(
675 "expecting rules to be a dict, got {value:?}"
676 ))
677 })?;
678
679 for (key, value) in dict {
680 rules.insert(key.to_string(), RulesValue::try_from(value)?);
681 }
682 }
683 "rules2" => {
684 let dict = value.as_dictionary().ok_or_else(|| {
685 AppleCodesignError::ResourcesPlistParse(format!(
686 "expecting rules2 to be a dict, got {value:?}"
687 ))
688 })?;
689
690 for (key, value) in dict {
691 rules2.insert(key.to_string(), Rules2Value::try_from(value)?);
692 }
693 }
694 key => {
695 return Err(AppleCodesignError::ResourcesPlistParse(format!(
696 "unexpected key in root dict: {key}"
697 )));
698 }
699 }
700 }
701
702 Ok(Self {
703 files,
704 files2,
705 rules,
706 rules2,
707 })
708 }
709
710 pub fn to_writer_xml(&self, mut writer: impl Write) -> Result<(), AppleCodesignError> {
712 let value = Value::from(self);
713
714 let mut data = Vec::<u8>::new();
719 value
720 .to_writer_xml(&mut data)
721 .map_err(AppleCodesignError::ResourcesPlist)?;
722
723 let data = String::from_utf8(data).expect("XML should be valid UTF-8");
724 let data = data.replace("<dict />", "<dict/>");
725 let data = data.replace("<true />", "<true/>");
726 let data = data.replace(""", "\"");
727
728 writer.write_all(data.as_bytes())?;
729 writer.write_all(b"\n")?;
730
731 Ok(())
732 }
733
734 pub fn add_rule(&mut self, rule: CodeResourcesRule) {
736 self.rules.insert(
737 rule.pattern,
738 RulesValue {
739 omit: rule.omit,
740 required: !rule.optional,
741 weight: rule.weight.map(|x| x as f64),
742 },
743 );
744 }
745
746 pub fn add_rule2(&mut self, rule: CodeResourcesRule) {
748 self.rules2.insert(
749 rule.pattern,
750 Rules2Value {
751 nested: if rule.nested { Some(true) } else { None },
752 omit: if rule.omit { Some(true) } else { None },
753 optional: if rule.optional { Some(true) } else { None },
754 weight: rule.weight.map(|x| x as f64),
755 },
756 );
757 }
758
759 pub fn seal_regular_file(
767 &mut self,
768 files_flavor: FilesFlavor,
769 path: impl ToString,
770 digests: MultiDigest,
771 optional: bool,
772 ) -> Result<(), AppleCodesignError> {
773 match files_flavor {
774 FilesFlavor::Rules => {
775 self.files.insert(
776 path.to_string(),
777 if optional {
778 FilesValue::Optional(digests.sha1.to_vec())
779 } else {
780 FilesValue::Required(digests.sha1.to_vec())
781 },
782 );
783
784 Ok(())
785 }
786 FilesFlavor::Rules2 => {
787 let hash2 = Some(digests.sha256.to_vec());
788
789 self.files2.insert(
790 path.to_string(),
791 Files2Value {
792 cdhash: None,
793 hash: None,
794 hash2,
795 optional: if optional { Some(true) } else { None },
796 requirement: None,
797 symlink: None,
798 },
799 );
800
801 Ok(())
802 }
803 FilesFlavor::Rules2WithSha1 => {
804 let hash = Some(digests.sha1.to_vec());
805 let hash2 = Some(digests.sha256.to_vec());
806
807 self.files2.insert(
808 path.to_string(),
809 Files2Value {
810 cdhash: None,
811 hash,
812 hash2,
813 optional: if optional { Some(true) } else { None },
814 requirement: None,
815 symlink: None,
816 },
817 );
818
819 Ok(())
820 }
821 }
822 }
823
824 pub fn seal_symlink(&mut self, path: impl ToString, target: impl ToString) {
828 self.files2.insert(
830 path.to_string(),
831 Files2Value {
832 cdhash: None,
833 hash: None,
834 hash2: None,
835 optional: None,
836 requirement: None,
837 symlink: Some(target.to_string()),
838 },
839 );
840 }
841
842 pub fn seal_macho(
846 &mut self,
847 path: impl ToString,
848 info: &SignedMachOInfo,
849 optional: bool,
850 ) -> Result<(), AppleCodesignError> {
851 self.files2.insert(
852 path.to_string(),
853 Files2Value {
854 cdhash: Some(DigestType::Sha256Truncated.digest_data(&info.code_directory_blob)?),
855 hash: None,
856 hash2: None,
857 optional: if optional { Some(true) } else { None },
858 requirement: info.designated_code_requirement.clone(),
859 symlink: None,
860 },
861 );
862
863 Ok(())
864 }
865}
866
867impl From<&CodeResources> for Value {
868 fn from(cr: &CodeResources) -> Self {
869 let mut dict = Dictionary::new();
870
871 dict.insert(
872 "files".to_string(),
873 Value::Dictionary(
874 cr.files
875 .iter()
876 .map(|(key, value)| (key.to_string(), Value::from(value)))
877 .collect::<Dictionary>(),
878 ),
879 );
880
881 dict.insert(
882 "files2".to_string(),
883 Value::Dictionary(
884 cr.files2
885 .iter()
886 .map(|(key, value)| (key.to_string(), Value::from(value)))
887 .collect::<Dictionary>(),
888 ),
889 );
890
891 if !cr.rules.is_empty() {
892 dict.insert(
893 "rules".to_string(),
894 Value::Dictionary(
895 cr.rules
896 .iter()
897 .map(|(key, value)| (key.to_string(), Value::from(value)))
898 .collect::<Dictionary>(),
899 ),
900 );
901 }
902
903 if !cr.rules2.is_empty() {
904 dict.insert(
905 "rules2".to_string(),
906 Value::Dictionary(
907 cr.rules2
908 .iter()
909 .map(|(key, value)| (key.to_string(), Value::from(value)))
910 .collect::<Dictionary>(),
911 ),
912 );
913 }
914
915 Value::Dictionary(dict)
916 }
917}
918
919pub fn normalized_resources_path(path: impl AsRef<Path>) -> String {
921 let path = path.as_ref().to_string_lossy().replace('\\', "/");
923
924 path.strip_prefix("Contents/").unwrap_or(&path).to_string()
928}
929
930fn find_rule(rules: &[CodeResourcesRule], path: impl AsRef<Path>) -> Option<CodeResourcesRule> {
939 let path = normalized_resources_path(path);
940 rules.iter().find(|rule| rule.re.is_match(&path)).cloned()
941}
942
943#[derive(Clone, Debug)]
949pub struct CodeResourcesBuilder {
950 rules: Vec<CodeResourcesRule>,
951 rules2: Vec<CodeResourcesRule>,
952 resources: CodeResources,
953 digests: Vec<DigestType>,
954}
955
956impl Default for CodeResourcesBuilder {
957 fn default() -> Self {
958 Self {
959 rules: vec![],
960 rules2: vec![],
961 resources: CodeResources::default(),
962 digests: vec![DigestType::Sha256],
963 }
964 }
965}
966
967impl CodeResourcesBuilder {
968 pub fn default_resources_rules() -> Result<Self, AppleCodesignError> {
970 let mut slf = Self::default();
971
972 slf.add_rule(CodeResourcesRule::new("^version.plist$")?);
973 slf.add_rule(CodeResourcesRule::new("^Resources/")?);
974 slf.add_rule(
975 CodeResourcesRule::new("^Resources/.*\\.lproj/")?
976 .optional()
977 .weight(1000),
978 );
979 slf.add_rule(CodeResourcesRule::new("^Resources/Base\\.lproj/")?.weight(1010));
980 slf.add_rule(
981 CodeResourcesRule::new("^Resources/.*\\.lproj/locversion.plist$")?
982 .omit()
983 .weight(1100),
984 );
985
986 slf.add_rule2(CodeResourcesRule::new("^.*")?);
987 slf.add_rule2(CodeResourcesRule::new("^[^/]+$")?.nested().weight(10));
988 slf.add_rule2(CodeResourcesRule::new("^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/")?
989 .nested().weight(10));
990 slf.add_rule2(CodeResourcesRule::new(".*\\.dSYM($|/)")?.weight(11));
991 slf.add_rule2(
992 CodeResourcesRule::new("^(.*/)?\\.DS_Store$")?
993 .omit()
994 .weight(2000),
995 );
996 slf.add_rule2(CodeResourcesRule::new("^Info\\.plist$")?.omit().weight(20));
997 slf.add_rule2(CodeResourcesRule::new("^version\\.plist$")?.weight(20));
998 slf.add_rule2(CodeResourcesRule::new("^embedded\\.provisionprofile$")?.weight(20));
999 slf.add_rule2(CodeResourcesRule::new("^PkgInfo$")?.omit().weight(20));
1000 slf.add_rule2(CodeResourcesRule::new("^Resources/")?.weight(20));
1001 slf.add_rule2(
1002 CodeResourcesRule::new("^Resources/.*\\.lproj/")?
1003 .optional()
1004 .weight(1000),
1005 );
1006 slf.add_rule2(CodeResourcesRule::new("^Resources/Base\\.lproj/")?.weight(1010));
1007 slf.add_rule2(
1008 CodeResourcesRule::new("^Resources/.*\\.lproj/locversion.plist$")?
1009 .omit()
1010 .weight(1100),
1011 );
1012
1013 Ok(slf)
1014 }
1015
1016 pub fn default_no_resources_rules() -> Result<Self, AppleCodesignError> {
1018 let mut slf = Self::default();
1019
1020 slf.add_rule(CodeResourcesRule::new("^version.plist$")?);
1021 slf.add_rule(CodeResourcesRule::new("^.*")?);
1022 slf.add_rule(
1023 CodeResourcesRule::new("^.*\\.lproj/")?
1024 .optional()
1025 .weight(1000),
1026 );
1027 slf.add_rule(CodeResourcesRule::new("^Base\\.lproj/")?.weight(1010));
1028 slf.add_rule(
1029 CodeResourcesRule::new("^.*\\.lproj/locversion.plist$")?
1030 .omit()
1031 .weight(1100),
1032 );
1033 slf.add_rule2(CodeResourcesRule::new("^.*")?);
1034 slf.add_rule2(CodeResourcesRule::new(".*\\.dSYM($|/)")?.weight(11));
1035 slf.add_rule2(
1036 CodeResourcesRule::new("^(.*/)?\\.DS_Store$")?
1037 .omit()
1038 .weight(2000),
1039 );
1040 slf.add_rule2(CodeResourcesRule::new("^Info\\.plist$")?.omit().weight(20));
1041 slf.add_rule2(CodeResourcesRule::new("^version\\.plist$")?.weight(20));
1042 slf.add_rule2(CodeResourcesRule::new("^embedded\\.provisionprofile$")?.weight(20));
1043 slf.add_rule2(CodeResourcesRule::new("^PkgInfo$")?.omit().weight(20));
1044 slf.add_rule2(
1045 CodeResourcesRule::new("^.*\\.lproj/")?
1046 .optional()
1047 .weight(1000),
1048 );
1049 slf.add_rule2(CodeResourcesRule::new("^Base\\.lproj/")?.weight(1010));
1050 slf.add_rule2(
1051 CodeResourcesRule::new("^.*\\.lproj/locversion.plist$")?
1052 .omit()
1053 .weight(1100),
1054 );
1055
1056 Ok(slf)
1057 }
1058
1059 pub fn set_digests(&mut self, digests: impl Iterator<Item = DigestType>) {
1061 self.digests = digests.collect::<Vec<_>>();
1062 }
1063
1064 pub fn add_rule(&mut self, rule: CodeResourcesRule) {
1066 self.rules.push(rule.clone());
1067 self.rules.sort();
1068 self.resources.add_rule(rule);
1069 }
1070
1071 pub fn add_rule2(&mut self, rule: CodeResourcesRule) {
1073 self.rules2.push(rule.clone());
1074 self.rules2.sort();
1075 self.resources.add_rule2(rule);
1076 }
1077
1078 pub fn add_exclusion_rule(&mut self, rule: CodeResourcesRule) {
1084 self.rules.push(rule.clone());
1085 self.rules.sort();
1086 self.rules2.push(rule);
1087 self.rules2.sort();
1088 }
1089
1090 pub fn walk_and_seal_directory(
1103 &mut self,
1104 root_bundle_path: &Path,
1105 bundle_root: &Path,
1106 context: &mut BundleSigningContext,
1107 ) -> Result<(), AppleCodesignError> {
1108 let mut skipping_rel_dirs = BTreeSet::new();
1109
1110 for entry in walkdir::WalkDir::new(bundle_root).sort_by_file_name() {
1111 let entry = entry?;
1112 let path = entry.path();
1113
1114 if path == bundle_root {
1115 continue;
1116 }
1117
1118 let rel_path = path
1119 .strip_prefix(bundle_root)
1120 .expect("stripping path prefix should always work");
1121 let root_rel_path_normalized = path
1122 .strip_prefix(root_bundle_path)
1123 .expect("stripping root prefix should always work")
1124 .to_string_lossy()
1125 .replace('\\', "/");
1126 let rel_path_normalized = normalized_resources_path(rel_path);
1127
1128 let file_name = rel_path
1129 .file_name()
1130 .expect("should have final path component")
1131 .to_string_lossy()
1132 .to_string();
1133
1134 if skipping_rel_dirs.iter().any(|p| rel_path.starts_with(p)) {
1136 debug!("{} ignored because marked as skipped", rel_path.display());
1137 continue;
1138 }
1139
1140 if let Some(rule) = find_rule(&self.rules2, rel_path) {
1142 debug!(
1143 "{}:{} matches rules2 {:?}",
1144 bundle_root.display(),
1145 rel_path.display(),
1146 rule
1147 );
1148
1149 if entry.file_type().is_dir() {
1150 if rule.nested {
1151 if file_name.contains('.') {
1153 self.seal_rules2_nested_bundle(
1158 path,
1159 rel_path,
1160 &rel_path_normalized,
1161 rule.optional,
1162 &context.dest_dir,
1163 )?;
1164
1165 skipping_rel_dirs.insert(rel_path.to_path_buf());
1166 }
1167 } else if rule.exclude {
1168 info!(
1169 "{} marked as excluded in resource rules",
1170 rel_path_normalized
1171 );
1172 skipping_rel_dirs.insert(rel_path.to_path_buf());
1173 }
1174
1175 } else if entry.file_type().is_file() {
1178 if rule.exclude {
1179 debug!("{} ignoring file due to exclude rule", rel_path_normalized);
1180 continue;
1181 }
1182
1183 if rule.nested {
1185 if crate::reader::path_is_macho(path)? {
1186 info!("sealing nested Mach-O binary: {}", rel_path.display());
1187
1188 self.seal_rules2_nested_macho(
1189 path,
1190 rel_path,
1191 &rel_path_normalized,
1192 &root_rel_path_normalized,
1193 context,
1194 rule.optional,
1195 )?;
1196 } else {
1197 error!(
1204 "encountered a non Mach-O file with a nested rule: {}",
1205 rel_path.display()
1206 );
1207 error!(
1208 "we do not know how to handle this scenario; either your bundle layout is invalid or you found a bug in this program"
1209 );
1210 error!(
1211 "if the bundle signs and verifies with Apple's tooling, consider reporting this issue"
1212 );
1213 }
1214 } else {
1215 self.seal_rules2_file(
1216 path,
1217 rel_path,
1218 &rel_path_normalized,
1219 &root_rel_path_normalized,
1220 rule.omit,
1221 rule.optional,
1222 context,
1223 )?;
1224 }
1225 } else if entry.file_type().is_symlink() {
1226 if rule.exclude {
1227 info!(
1228 "{} ignoring symlink due to exclude rule",
1229 rel_path_normalized
1230 );
1231 continue;
1232 }
1233
1234 self.seal_rules2_symlink(
1235 path,
1236 rel_path,
1237 &rel_path_normalized,
1238 rule.omit,
1239 context,
1240 )?;
1241 } else {
1242 warn!(
1243 "{} unexpected file type encountering during bundle signing",
1244 rel_path_normalized
1245 );
1246 }
1247 } else {
1248 debug!(
1249 "{}:{} doesn't match any rules2 rule",
1250 bundle_root.display(),
1251 rel_path.display()
1252 );
1253 }
1254
1255 if let Some(rule) = find_rule(&self.rules, rel_path) {
1258 debug!(
1259 "{}:{} matches rules rule {:?}",
1260 bundle_root.display(),
1261 rel_path.display(),
1262 rule
1263 );
1264
1265 if entry.file_type().is_file() {
1266 if rule.exclude {
1267 continue;
1268 }
1269
1270 self.seal_rules1_file(path, &rel_path_normalized, rule)?;
1271 }
1272 }
1273 }
1274
1275 Ok(())
1276 }
1277
1278 fn seal_rules2_nested_bundle(
1280 &mut self,
1281 full_path: &Path,
1282 rel_path: &Path,
1283 rel_path_normalized: &str,
1284 optional: bool,
1285 dest_dir: &Path,
1286 ) -> Result<(), AppleCodesignError> {
1287 info!(
1288 "sealing nested directory as a bundle: {}",
1289 rel_path.display()
1290 );
1291 let bundle = DirectoryBundle::new_from_path(full_path)?;
1292
1293 if let Some(nested_exe) = bundle
1294 .files(false)?
1295 .into_iter()
1296 .find(|f| matches!(f.is_main_executable(), Ok(true)))
1297 {
1298 let nested_exe = dest_dir.join(rel_path).join(nested_exe.relative_path());
1299
1300 info!("reading Mach-O signature from {}", nested_exe.display());
1301 let macho_data = isideload_vfs::fs::read(&nested_exe)?;
1302 let macho_info = SignedMachOInfo::parse_data(&macho_data)?;
1303
1304 self.resources
1305 .seal_macho(rel_path_normalized, &macho_info, optional)?;
1306 } else {
1307 warn!(
1308 "could not find main executable of presumed nested bundle: {}",
1309 rel_path.display()
1310 );
1311 }
1312
1313 Ok(())
1314 }
1315
1316 fn seal_rules2_nested_macho(
1318 &mut self,
1319 full_path: &Path,
1320 rel_path: &Path,
1321 rel_path_normalized: &str,
1322 root_rel_path: &str,
1323 context: &mut BundleSigningContext,
1324 optional: bool,
1325 ) -> Result<(), AppleCodesignError> {
1326 let macho_info = if context
1327 .settings
1328 .path_exclusion_pattern_matches(root_rel_path)
1329 {
1330 warn!(
1331 "skipping signing of nested Mach-O binary because excluded by settings: {}",
1332 rel_path.display()
1333 );
1334 warn!("(an error will occur if this binary is not already signed)");
1335 warn!(
1336 "(if you see an error, sign that Mach-O explicitly or remove it from the exclusion settings)"
1337 );
1338
1339 let dest_path = context.install_file(full_path, rel_path)?;
1340 let data = isideload_vfs::fs::read(dest_path)?;
1341
1342 SignedMachOInfo::parse_data(&data)?
1343 } else {
1344 context.sign_and_install_macho(full_path, rel_path)?.1
1345 };
1346
1347 self.resources
1348 .seal_macho(rel_path_normalized, &macho_info, optional)
1349 }
1350
1351 fn seal_rules2_file(
1353 &mut self,
1354 full_path: &Path,
1355 rel_path: &Path,
1356 rel_path_normalized: &str,
1357 root_rel_path: &str,
1358 omit: bool,
1359 optional: bool,
1360 context: &mut BundleSigningContext,
1361 ) -> Result<(), AppleCodesignError> {
1362 let mut need_install = !context.previously_installed_paths.contains(rel_path);
1363 if !omit {
1365 let sign_macho = need_install
1375 && crate::reader::path_is_macho(full_path)?
1376 && !context
1377 .settings
1378 .path_exclusion_pattern_matches(root_rel_path)
1379 && !context.settings.shallow();
1380
1381 let read_path = if sign_macho {
1382 info!(
1383 "non-nested file is a Mach-O binary; signing accordingly {}",
1384 rel_path.display()
1385 );
1386 need_install = false;
1387 context.sign_and_install_macho(full_path, rel_path)?.0
1390 } else {
1391 info!("sealing regular file {}", rel_path_normalized);
1392
1393 if need_install {
1400 full_path.to_path_buf()
1401 } else {
1402 context.dest_dir.join(rel_path)
1403 }
1404 };
1405
1406 let digests = MultiDigest::from_path(read_path)?;
1407
1408 let flavor = if self.digests.contains(&DigestType::Sha1) {
1409 FilesFlavor::Rules2WithSha1
1410 } else {
1411 FilesFlavor::Rules2
1412 };
1413
1414 self.resources
1417 .seal_regular_file(flavor, rel_path_normalized, digests, optional)?;
1418 }
1419
1420 if need_install {
1421 context.install_file(full_path, rel_path)?;
1422 }
1423
1424 Ok(())
1425 }
1426
1427 fn seal_rules2_symlink(
1428 &mut self,
1429 full_path: &Path,
1430 rel_path: &Path,
1431 rel_path_normalized: &str,
1432 omit: bool,
1433 context: &mut BundleSigningContext,
1434 ) -> Result<(), AppleCodesignError> {
1435 let link_target = isideload_vfs::fs::read_link(full_path)?
1436 .to_string_lossy()
1437 .replace('\\', "/");
1438
1439 if !omit {
1440 info!("sealing symlink {} -> {}", rel_path_normalized, link_target);
1441 self.resources
1442 .seal_symlink(rel_path_normalized, link_target);
1443 }
1444 context.install_file(full_path, rel_path)?;
1445
1446 Ok(())
1447 }
1448
1449 fn seal_rules1_file(
1451 &mut self,
1452 full_path: &Path,
1453 rel_path_normalized: &str,
1454 rule: CodeResourcesRule,
1455 ) -> Result<(), AppleCodesignError> {
1456 let digests = MultiDigest::from_path(full_path)?;
1461
1462 self.resources.seal_regular_file(
1463 FilesFlavor::Rules,
1464 rel_path_normalized,
1465 digests,
1466 rule.optional,
1467 )?;
1468
1469 Ok(())
1470 }
1471
1472 pub fn write_code_resources(&self, writer: impl Write) -> Result<(), AppleCodesignError> {
1474 self.resources.to_writer_xml(writer)
1475 }
1476}
1477
1478#[cfg(test)]
1479mod tests {
1480 use super::*;
1481
1482 const FIREFOX_SNIPPET: &str = r#"
1483 <?xml version="1.0" encoding="UTF-8"?>
1484 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1485 <plist version="1.0">
1486 <dict>
1487 <key>files</key>
1488 <dict>
1489 <key>Resources/XUL.sig</key>
1490 <data>Y0SEPxyC6hCQ+rl4LTRmXy7F9DQ=</data>
1491 <key>Resources/en.lproj/InfoPlist.strings</key>
1492 <dict>
1493 <key>hash</key>
1494 <data>U8LTYe+cVqPcBu9aLvcyyfp+dAg=</data>
1495 <key>optional</key>
1496 <true/>
1497 </dict>
1498 <key>Resources/firefox-bin.sig</key>
1499 <data>ZvZ3yDciAF4kB9F06Xr3gKi3DD4=</data>
1500 </dict>
1501 <key>files2</key>
1502 <dict>
1503 <key>Library/LaunchServices/org.mozilla.updater</key>
1504 <dict>
1505 <key>hash2</key>
1506 <data>iMnDHpWkKTI6xLi9Av93eNuIhxXhv3C18D4fljCfw2Y=</data>
1507 </dict>
1508 <key>TestOptional</key>
1509 <dict>
1510 <key>hash2</key>
1511 <data>iMnDHpWkKTI6xLi9Av93eNuIhxXhv3C18D4fljCfw2Y=</data>
1512 <key>optional</key>
1513 <true/>
1514 </dict>
1515 <key>MacOS/XUL</key>
1516 <dict>
1517 <key>cdhash</key>
1518 <data>NevNMzQBub9OjomMUAk2xBumyHM=</data>
1519 <key>requirement</key>
1520 <string>anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "43AQ936H96"</string>
1521 </dict>
1522 <key>MacOS/SafariForWebKitDevelopment</key>
1523 <dict>
1524 <key>symlink</key>
1525 <string>/Library/Application Support/Apple/Safari/SafariForWebKitDevelopment</string>
1526 </dict>
1527 </dict>
1528 <key>rules</key>
1529 <dict>
1530 <key>^Resources/</key>
1531 <true/>
1532 <key>^Resources/.*\.lproj/</key>
1533 <dict>
1534 <key>optional</key>
1535 <true/>
1536 <key>weight</key>
1537 <real>1000</real>
1538 </dict>
1539 </dict>
1540 <key>rules2</key>
1541 <dict>
1542 <key>.*\.dSYM($|/)</key>
1543 <dict>
1544 <key>weight</key>
1545 <real>11</real>
1546 </dict>
1547 <key>^(.*/)?\.DS_Store$</key>
1548 <dict>
1549 <key>omit</key>
1550 <true/>
1551 <key>weight</key>
1552 <real>2000</real>
1553 </dict>
1554 <key>^[^/]+$</key>
1555 <dict>
1556 <key>nested</key>
1557 <true/>
1558 <key>weight</key>
1559 <real>10</real>
1560 </dict>
1561 <key>optional</key>
1562 <dict>
1563 <key>optional</key>
1564 <true/>
1565 </dict>
1566 </dict>
1567 </dict>
1568 </plist>"#;
1569
1570 #[test]
1571 fn parse_firefox() {
1572 let resources = CodeResources::from_xml(FIREFOX_SNIPPET.as_bytes()).unwrap();
1573
1574 let mut buffer = Vec::<u8>::new();
1576 resources.to_writer_xml(&mut buffer).unwrap();
1577 let resources2 = CodeResources::from_xml(&buffer).unwrap();
1578
1579 assert_eq!(resources, resources2);
1580 }
1581}