Skip to content

Commit e04ddb2

Browse files
committed
apple-codesign: prevent double signing / installing files when traversing bundles
Previously, it was possible to encounter a file multiple times when signing a bundle containing child bundles. Normally we ignore traversing into an already signed bundle. But this only works if the bundle matches a CodeResources rule having the "nested" flag set. If a child bundle were outside a "nested" directory, we would walk into it just like any other directory. It is important for us to walk into these non-"nested" child bundles because the file content needs to be sealed - as files, not a bundle. But this posed a problem with Mach-O binaries. If we signed a Mach-O binary as part of a child bundle and then encountered that Mach-O binary again as part of walking a non-"nested" child bundle, we would sign the Mach-O again! In many scenarios this would break the child bundle's signature since the Mach-O content changed. And for non-Macho-O files, we may copy/write the destination file twice, adding overhead to signing. This commit introduces tracking of which files are installed when signing a bundle. When signing a bundle, we track if a path has already been installed. If so, we seal the already-written file (not the source file!) and skip installing the file again. This generic approach seems to magically fix issues with non-"nested" child bundles. The test changes clearly show that we stop double signing Mach-O binaries located in non-"nested" child bundles. In the case of the child bundle's main binary, the new signature correctly contains an Info.plist and CodeResources file digests. This is because the original signed binary is preserved. We also see that the tree of installed files is unchanged modulo the a) now single-signed Mach-O, b) the CodeResources of the parent bundle, and c) the parent bundle binary's signature. This all makes sense. Closes #149.
1 parent d6ec861 commit e04ddb2

File tree

5 files changed

+209
-105
lines changed

5 files changed

+209
-105
lines changed

apple-codesign/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ Released on ReleaseDate.
99
* When signing a bundle in `--shallow` mode, we no longer sign Mach-O binaries
1010
that aren't the *main* bundle binary. The new behavior is compatible with the
1111
behavior of Apple's `codesign`. (#148)
12+
* Fixed a bug where signing of a bundle containing child bundles could sign and
13+
install certain files multiple times. This could result in a child bundle having
14+
an incorrect signature. (#149)
1215
* MSRV 1.78 -> 1.81.
1316
* `aws-sdk-s3` 1.24 -> 1.59.
1417
* `clap` 4.4 -> 4.5.

apple-codesign/src/bundle_signing.rs

Lines changed: 117 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use {
2222
simple_file_manifest::create_symlink,
2323
std::{
2424
borrow::Cow,
25-
collections::BTreeMap,
25+
collections::{BTreeMap, BTreeSet},
2626
io::Write,
2727
path::{Path, PathBuf},
2828
},
@@ -33,12 +33,19 @@ use {
3333
/// This does not use the CodeResources rules for a bundle. Rather, it
3434
/// blindly copies all files in the bundle. This means that excluded files
3535
/// can be copied.
36-
pub fn copy_bundle(bundle: &DirectoryBundle, dest_dir: &Path) -> Result<(), AppleCodesignError> {
36+
///
37+
/// Returns the set of bundle-relative paths that are installed.
38+
pub fn copy_bundle(
39+
bundle: &DirectoryBundle,
40+
dest_dir: &Path,
41+
) -> Result<BTreeSet<PathBuf>, AppleCodesignError> {
3742
let settings = SigningSettings::default();
3843

39-
let context = BundleSigningContext {
44+
let mut context = BundleSigningContext {
4045
dest_dir: dest_dir.to_path_buf(),
4146
settings: &settings,
47+
previously_installed_paths: Default::default(),
48+
installed_paths: Default::default(),
4249
};
4350

4451
for file in bundle
@@ -48,7 +55,7 @@ pub fn copy_bundle(bundle: &DirectoryBundle, dest_dir: &Path) -> Result<(), Appl
4855
context.install_file(file.absolute_path(), file.relative_path())?;
4956
}
5057

51-
Ok(())
58+
Ok(context.installed_paths)
5259
}
5360

5461
/// A primitive for signing an Apple bundle.
@@ -186,22 +193,63 @@ impl BundleSigner {
186193
}
187194
}
188195

196+
// We keep track of root relative input paths that have been installed so we can
197+
// skip installing in case we encounter the path again when signing a parent
198+
// bundle.
199+
//
200+
// If we fail to do this, during non-shallow signing operations we may descend
201+
// into a child bundle that is outside a directory with the "nested" flag set.
202+
// Files in non-"nested" directories need to be sealed in CodeResources files
203+
// as regular files, not bundles. So we need to walk into the child bundle in
204+
// this scenario. But during the walk we want to prevent already installed files
205+
// from being processed again.
206+
//
207+
// In the case of Mach-O binaries in the above non-"nested" directory scenario,
208+
// excluding already signed files prevents the Mach-O from being signed again.
209+
// Signing the Mach-O twice could invalidate the bundle's signature and/or result
210+
// in incorrect signing settings since a bundle's main binary wouldn't be
211+
// recognized as such since we're outside the context of that bundle.
212+
//
213+
// In all cases, we prevent redundant work installing files if a file is seen
214+
// twice.
215+
let mut installed_rel_paths = BTreeSet::<PathBuf>::new();
216+
189217
for (rel, nested) in bundles {
218+
let rel_path = PathBuf::from(rel);
219+
190220
let nested_dest_dir = dest_dir.join(rel);
191221
warn!("entering nested bundle {}", rel,);
192222

193-
if settings.shallow() {
223+
let bundle_installed_rel_paths = if settings.shallow() {
194224
warn!("shallow signing enabled; bundle will be copied instead of signed");
195-
copy_bundle(&nested.bundle, &nested_dest_dir)?;
225+
copy_bundle(&nested.bundle, &nested_dest_dir)?
196226
} else if settings.path_exclusion_pattern_matches(rel) {
197227
// If we excluded this bundle from signing, just copy all the files.
198228
warn!("bundle is in exclusion list; it will be copied instead of signed");
199-
copy_bundle(&nested.bundle, &nested_dest_dir)?;
229+
copy_bundle(&nested.bundle, &nested_dest_dir)?
200230
} else {
201-
nested.write_signed_bundle(
231+
let bundle_installed = installed_rel_paths
232+
.iter()
233+
.filter_map(|p| {
234+
if let Ok(p) = p.strip_prefix(&rel_path) {
235+
Some(p.to_path_buf())
236+
} else {
237+
None
238+
}
239+
})
240+
.collect::<BTreeSet<_>>();
241+
242+
let info = nested.write_signed_bundle(
202243
nested_dest_dir,
203244
&settings.as_nested_bundle_settings(rel),
245+
bundle_installed,
204246
)?;
247+
248+
info.installed_rel_paths
249+
};
250+
251+
for p in bundle_installed_rel_paths {
252+
installed_rel_paths.insert(rel_path.join(p).to_path_buf());
205253
}
206254

207255
warn!("leaving nested bundle {}", rel);
@@ -212,7 +260,9 @@ impl BundleSigner {
212260
.get(&None)
213261
.expect("main bundle should have a key");
214262

215-
main.write_signed_bundle(dest_dir, settings)
263+
Ok(main
264+
.write_signed_bundle(dest_dir, settings, installed_rel_paths)?
265+
.bundle)
216266
}
217267
}
218268

@@ -343,16 +393,22 @@ pub struct BundleSigningContext<'a, 'key> {
343393
pub settings: &'a SigningSettings<'key>,
344394
/// Where the bundle is getting installed to.
345395
pub dest_dir: PathBuf,
396+
/// Bundle relative paths of files that have already been installed.
397+
///
398+
/// The already-present destination file content should be used for sealing.
399+
pub previously_installed_paths: BTreeSet<PathBuf>,
400+
/// Bundle relative paths of files that are installed by this signing operation.
401+
pub installed_paths: BTreeSet<PathBuf>,
346402
}
347403

348404
impl<'a, 'key> BundleSigningContext<'a, 'key> {
349405
/// Install a file (regular or symlink) in the destination directory.
350406
pub fn install_file(
351-
&self,
407+
&mut self,
352408
source_path: &Path,
353-
dest_rel_path: &Path,
409+
bundle_rel_path: &Path,
354410
) -> Result<PathBuf, AppleCodesignError> {
355-
let dest_path = self.dest_dir.join(dest_rel_path);
411+
let dest_path = self.dest_dir.join(bundle_rel_path);
356412

357413
if source_path != dest_path {
358414
// Remove an existing file before installing the replacement. In
@@ -394,6 +450,11 @@ impl<'a, 'key> BundleSigningContext<'a, 'key> {
394450
}
395451
}
396452

453+
// Always record the installation even if we no-op. The intent of the
454+
// annotation is to mark files that are already present in the destination
455+
// bundle.
456+
self.installed_paths.insert(bundle_rel_path.to_path_buf());
457+
397458
Ok(dest_path)
398459
}
399460

@@ -402,24 +463,24 @@ impl<'a, 'key> BundleSigningContext<'a, 'key> {
402463
/// Returns Mach-O metadata which can be recorded in a CodeResources file.
403464
404465
pub fn sign_and_install_macho(
405-
&self,
466+
&mut self,
406467
source_path: &Path,
407-
dest_rel_path: &Path,
468+
bundle_rel_path: &Path,
408469
) -> Result<(PathBuf, SignedMachOInfo), AppleCodesignError> {
409-
warn!("signing Mach-O file {}", dest_rel_path.display());
470+
warn!("signing Mach-O file {}", bundle_rel_path.display());
410471

411472
let macho_data = std::fs::read(source_path)?;
412473
let signer = MachOSigner::new(&macho_data)?;
413474

414475
let mut settings = self
415476
.settings
416-
.as_bundle_macho_settings(dest_rel_path.to_string_lossy().as_ref());
477+
.as_bundle_macho_settings(bundle_rel_path.to_string_lossy().as_ref());
417478

418479
// When signing a Mach-O in the context of a bundle, always define the
419480
// binary identifier from the filename so everything is consistent.
420481
// Unless an existing setting overrides it, of course.
421482
if settings.binary_identifier(SettingsScope::Main).is_none() {
422-
let identifier = path_identifier(dest_rel_path)?;
483+
let identifier = path_identifier(bundle_rel_path)?;
423484
info!("setting binary identifier based on path: {}", identifier);
424485

425486
settings.set_binary_identifier(SettingsScope::Main, &identifier);
@@ -430,17 +491,28 @@ impl<'a, 'key> BundleSigningContext<'a, 'key> {
430491
let mut new_data = Vec::<u8>::with_capacity(macho_data.len() + 2_usize.pow(17));
431492
signer.write_signed_binary(&settings, &mut new_data)?;
432493

433-
let dest_path = self.dest_dir.join(dest_rel_path);
494+
let dest_path = self.dest_dir.join(bundle_rel_path);
434495

435496
info!("writing Mach-O to {}", dest_path.display());
436497
write_macho_file(source_path, &dest_path, &new_data)?;
437498

438499
let info = SignedMachOInfo::parse_data(&new_data)?;
439500

501+
self.installed_paths.insert(bundle_rel_path.to_path_buf());
502+
440503
Ok((dest_path, info))
441504
}
442505
}
443506

507+
/// Holds metadata describing the result of a bundle signing operation.
508+
pub struct BundleSigningInfo {
509+
/// The signed bundle.
510+
pub bundle: DirectoryBundle,
511+
512+
/// Bundle relative paths of files that are installed by this signing operation.
513+
pub installed_rel_paths: BTreeSet<PathBuf>,
514+
}
515+
444516
/// A primitive for signing a single Apple bundle.
445517
///
446518
/// Unlike [BundleSigner], this type only signs a single bundle and is ignorant
@@ -469,7 +541,8 @@ impl SingleBundleSigner {
469541
&self,
470542
dest_dir: impl AsRef<Path>,
471543
settings: &SigningSettings,
472-
) -> Result<DirectoryBundle, AppleCodesignError> {
544+
previously_installed_paths: BTreeSet<PathBuf>,
545+
) -> Result<BundleSigningInfo, AppleCodesignError> {
473546
let dest_dir = dest_dir.as_ref();
474547

475548
warn!(
@@ -499,9 +572,11 @@ impl SingleBundleSigner {
499572
// But we still need to preserve files (hopefully just symlinks) outside the
500573
// nested bundles under `Versions/`. Since we don't nest into child bundles
501574
// here, it should be safe to handle each encountered file.
502-
let context = BundleSigningContext {
575+
let mut context = BundleSigningContext {
503576
dest_dir: dest_dir.to_path_buf(),
504577
settings,
578+
previously_installed_paths,
579+
installed_paths: Default::default(),
505580
};
506581

507582
for file in self
@@ -512,8 +587,13 @@ impl SingleBundleSigner {
512587
context.install_file(file.absolute_path(), file.relative_path())?;
513588
}
514589

515-
return DirectoryBundle::new_from_path(dest_dir)
516-
.map_err(AppleCodesignError::DirectoryBundle);
590+
let bundle = DirectoryBundle::new_from_path(dest_dir)
591+
.map_err(AppleCodesignError::DirectoryBundle)?;
592+
593+
return Ok(BundleSigningInfo {
594+
bundle,
595+
installed_rel_paths: context.installed_paths,
596+
});
517597
} else {
518598
info!("found an unversioned framework; signing like normal");
519599
}
@@ -609,15 +689,17 @@ impl SingleBundleSigner {
609689
);
610690
}
611691

612-
let context = BundleSigningContext {
692+
let mut context = BundleSigningContext {
613693
dest_dir: dest_dir_root.clone(),
614694
settings,
695+
previously_installed_paths,
696+
installed_paths: Default::default(),
615697
};
616698

617699
resources_builder.walk_and_seal_directory(
618700
&self.root_bundle_path,
619701
self.bundle.root_dir(),
620-
&context,
702+
&mut context,
621703
)?;
622704

623705
let info_plist_data = std::fs::read(self.bundle.info_plist_path())?;
@@ -674,10 +756,20 @@ impl SingleBundleSigner {
674756
let dest_path = dest_dir_root.join(exe.relative_path());
675757
info!("writing signed main executable to {}", dest_path.display());
676758
write_macho_file(exe.absolute_path(), &dest_path, &new_data)?;
759+
760+
context
761+
.installed_paths
762+
.insert(exe.relative_path().to_path_buf());
677763
} else {
678764
warn!("bundle has no main executable to sign specially");
679765
}
680766

681-
DirectoryBundle::new_from_path(&dest_dir_root).map_err(AppleCodesignError::DirectoryBundle)
767+
let bundle = DirectoryBundle::new_from_path(&dest_dir_root)
768+
.map_err(AppleCodesignError::DirectoryBundle)?;
769+
770+
Ok(BundleSigningInfo {
771+
bundle,
772+
installed_rel_paths: context.installed_paths,
773+
})
682774
}
683775
}

apple-codesign/src/code_resources.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,7 +1104,7 @@ impl CodeResourcesBuilder {
11041104
&mut self,
11051105
root_bundle_path: &Path,
11061106
bundle_root: &Path,
1107-
context: &BundleSigningContext,
1107+
context: &mut BundleSigningContext,
11081108
) -> Result<(), AppleCodesignError> {
11091109
let mut skipping_rel_dirs = BTreeSet::new();
11101110

@@ -1317,7 +1317,7 @@ impl CodeResourcesBuilder {
13171317
rel_path: &Path,
13181318
rel_path_normalized: &str,
13191319
root_rel_path: &str,
1320-
context: &BundleSigningContext,
1320+
context: &mut BundleSigningContext,
13211321
optional: bool,
13221322
) -> Result<(), AppleCodesignError> {
13231323
let macho_info = if context
@@ -1352,12 +1352,11 @@ impl CodeResourcesBuilder {
13521352
root_rel_path: &str,
13531353
omit: bool,
13541354
optional: bool,
1355-
context: &BundleSigningContext,
1355+
context: &mut BundleSigningContext,
13561356
) -> Result<(), AppleCodesignError> {
1357-
let mut need_install = true;
1357+
let mut need_install = !context.previously_installed_paths.contains(rel_path);
13581358

1359-
// Only seal if the omit flag is unset. But install unconditionally
1360-
// in all cases.
1359+
// Only seal if the omit flag is unset.
13611360
if !omit {
13621361
// Unlike Apple's tooling, we recognize Mach-O binaries when the nested
13631362
// flag isn't set and we automatically sign.
@@ -1368,7 +1367,8 @@ impl CodeResourcesBuilder {
13681367
// The reason we exclude in shallow mode is that shallow mode is supposed
13691368
// to behave like Apple's `codesign` and that tool only signs the bundle's
13701369
// "main" Mach-O binary, not other binaries.
1371-
let sign_macho = crate::reader::path_is_macho(full_path)?
1370+
let sign_macho = need_install
1371+
&& crate::reader::path_is_macho(full_path)?
13721372
&& !context
13731373
.settings
13741374
.path_exclusion_pattern_matches(root_rel_path)
@@ -1385,7 +1385,18 @@ impl CodeResourcesBuilder {
13851385
context.sign_and_install_macho(full_path, rel_path)?.0
13861386
} else {
13871387
info!("sealing regular file {}", rel_path_normalized);
1388-
full_path.to_path_buf()
1388+
1389+
// If we need to install the file, seal the source file. Else since the
1390+
// file is already installed, seal the destination file.
1391+
//
1392+
// For regular files this distinction doesn't matter. But for Mach-O
1393+
// binaries it ensures we pick up the final signature, not the source
1394+
// file.
1395+
if need_install {
1396+
full_path.to_path_buf()
1397+
} else {
1398+
context.dest_dir.join(rel_path)
1399+
}
13891400
};
13901401

13911402
let digests = MultiDigest::from_path(read_path)?;
@@ -1415,7 +1426,7 @@ impl CodeResourcesBuilder {
14151426
rel_path: &Path,
14161427
rel_path_normalized: &str,
14171428
omit: bool,
1418-
context: &BundleSigningContext,
1429+
context: &mut BundleSigningContext,
14191430
) -> Result<(), AppleCodesignError> {
14201431
let link_target = std::fs::read_link(full_path)?
14211432
.to_string_lossy()

0 commit comments

Comments
 (0)