Skip to content

Commit

Permalink
apple-codesign: implement binary identifier naming heuristics
Browse files Browse the repository at this point in the history
Our heuristics differed from Apple's, which have more complex behavior
for how digits and dots are handled.

Part of #98.
  • Loading branch information
indygreg committed Nov 12, 2023
1 parent 884fa47 commit 7d20f36
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 38 deletions.
4 changes: 4 additions & 0 deletions apple-codesign/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Released on ReleaseDate.
instead of using a mix of `--foo-path`, `--foo-filename`, and
potentially other variants. The old names are still recognized as
aliases to maintain backwards compatibility.
* Changed heuristic for naming a binary identifier from its path to be
more similar to Apple's. e.g. `foo1.2.dylib` will now resolve to `foo1`
instead of `foo1.2`. We still don't use the binary UUID or digest of its
load commands to compute the binary identifier like Apple does.

## 0.24.0

Expand Down
10 changes: 2 additions & 8 deletions apple-codesign/src/bundle_signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use {
error::AppleCodesignError,
macho::MachFile,
macho_signing::{write_macho_file, MachOSigner},
signing::path_identifier,
signing_settings::{SettingsScope, SigningSettings},
},
apple_bundles::{BundlePackageType, DirectoryBundle},
Expand Down Expand Up @@ -388,14 +389,7 @@ impl<'a, 'key> BundleSigningContext<'a, 'key> {
// and we avoid a signing error due to missing identifier.
// TODO do we need to check the nested Mach-O settings?
if settings.binary_identifier(SettingsScope::Main).is_none() {
let identifier = dest_rel_path
.file_name()
.expect("failure to extract filename (this should never happen)")
.to_string_lossy();

let identifier = identifier
.strip_suffix(".dylib")
.unwrap_or_else(|| identifier.as_ref());
let identifier = path_identifier(dest_rel_path)?;

info!(
"Mach-O is missing binary identifier; setting to {} based on file name",
Expand Down
3 changes: 3 additions & 0 deletions apple-codesign/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ pub enum AppleCodesignError {
#[error("unspecified digest error")]
DigestUnspecified,

#[error("deriving identifier from path: {0}")]
PathIdentifier(String),

#[error("error interfacing with directory-based bundle: {0}")]
DirectoryBundle(anyhow::Error),

Expand Down
109 changes: 101 additions & 8 deletions apple-codesign/src/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,7 @@ impl<'key> UnifiedSigner<'key> {
settings.import_settings_from_macho(&macho_data)?;

if settings.binary_identifier(SettingsScope::Main).is_none() {
let identifier = input_path
.file_name()
.ok_or_else(|| {
AppleCodesignError::CliGeneralError(
"unable to resolve file name of binary".into(),
)
})?
.to_string_lossy();
let identifier = path_identifier(input_path)?;

warn!("setting binary identifier to {}", identifier);
settings.set_binary_identifier(SettingsScope::Main, identifier);
Expand Down Expand Up @@ -227,3 +220,103 @@ impl<'key> UnifiedSigner<'key> {
Ok(())
}
}

pub fn path_identifier(path: impl AsRef<Path>) -> Result<String, AppleCodesignError> {
let path = path.as_ref();

// We only care about the file name.
let file_name = path
.file_name()
.ok_or_else(|| {
AppleCodesignError::PathIdentifier(format!("path {} lacks a file name", path.display()))
})?
.to_string_lossy()
.to_string();

// Remove the final file extension unless it is numeric.
let id = if let Some((prefix, extension)) = file_name.rsplit_once('.') {
if extension.chars().all(|c| c.is_ascii_digit()) {
file_name.as_str()
} else {
prefix
}
} else {
file_name.as_str()
};

let is_digit_or_dot = |c: char| c == '.' || c.is_ascii_digit();

// If begins with digit or dot, use as is, handling empty string special
// case.
let id = match id.chars().next() {
Some(first) => {
if is_digit_or_dot(first) {
return Ok(id.to_string());
} else {
id
}
}
None => {
return Ok(id.to_string());
}
};

// Strip all components having numeric *suffixes* except the first
// one. This doesn't strip extension components but *suffixes*. So
// e.g. libFoo1.2.3 -> libFoo1. Logically, we strip trailing digits
// + dot after the first dot preceded by digits.

let prefix = id.trim_end_matches(is_digit_or_dot);
let stripped = &id[prefix.len()..];

if stripped.is_empty() {
Ok(id.to_string())
} else {
// If the next character is a dot, add it back in.
let (prefix, stripped) = if matches!(stripped.chars().next(), Some('.')) {
(&id[0..prefix.len() + 1], &stripped[1..])
} else {
(prefix, stripped)
};

// Add back in any leading digits.

let id = prefix
.chars()
.chain(stripped.chars().take_while(|c| c.is_ascii_digit()))
.collect::<String>();

Ok(id)
}
}

#[cfg(test)]
mod test {
use super::*;
#[test]
fn path_identifier_normalization() {
assert_eq!(path_identifier("foo").unwrap(), "foo");
assert_eq!(path_identifier("foo.dylib").unwrap(), "foo");
assert_eq!(path_identifier("/etc/foo.dylib").unwrap(), "foo");
assert_eq!(path_identifier("/etc/foo").unwrap(), "foo");

// Starts with digit or dot is preserved module final extension.
assert_eq!(path_identifier(".foo").unwrap(), "");
assert_eq!(path_identifier("123").unwrap(), "123");
assert_eq!(path_identifier(".foo.dylib").unwrap(), ".foo");
assert_eq!(path_identifier("123.dylib").unwrap(), "123");
assert_eq!(path_identifier("123.42").unwrap(), "123.42");

// Digit final extension preserved.

assert_eq!(path_identifier("foo1").unwrap(), "foo1");
assert_eq!(path_identifier("foo1.dylib").unwrap(), "foo1");
assert_eq!(path_identifier("foo1.2.dylib").unwrap(), "foo1");
assert_eq!(path_identifier("foo1.2").unwrap(), "foo1");
assert_eq!(path_identifier("foo1.2.3.4.dylib").unwrap(), "foo1");
assert_eq!(path_identifier("foo.1").unwrap(), "foo.1");
assert_eq!(path_identifier("foo.1.2.3").unwrap(), "foo.1");
assert_eq!(path_identifier("foo.1.2.dylib").unwrap(), "foo.1");
assert_eq!(path_identifier("foo.1.dylib").unwrap(), "foo.1");
}
}
2 changes: 1 addition & 1 deletion apple-codesign/tests/cmd/sign-binary-identifier.trycmd
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ $ rcodesign print-signature-info exe.signed
$ rcodesign sign exe.signed exe.signed.2
signing exe.signed to exe.signed.2
signing exe.signed as a Mach-O binary
setting binary identifier to exe.signed
setting binary identifier to exe
parsing Mach-O
writing Mach-O to exe.signed.2

Expand Down
38 changes: 19 additions & 19 deletions apple-codesign/tests/cmd/sign-bundle-nested-symlinks.trycmd
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,38 @@ $ rcodesign debug-file-tree MyApp.app.signed
d MyApp.app.signed/
d MyApp.app.signed/Contents
d MyApp.app.signed/Contents/Frameworks
f 95566b60f24ce6600306 MyApp.app.signed/Contents/Frameworks/libssh.4.8.8.dylib
f 40a8b05ac0eac09b1186 MyApp.app.signed/Contents/Frameworks/libssh.4.8.8.dylib
l MyApp.app.signed/Contents/Frameworks/libssh.4.dylib -> libssh.4.8.8.dylib
l MyApp.app.signed/Contents/Frameworks/libssh.dylib -> libssh.4.dylib
f 0a5902dc8e47f490d038 MyApp.app.signed/Contents/Info.plist
d MyApp.app.signed/Contents/MacOS
f 0b577ed98ce1915f2d29 MyApp.app.signed/Contents/MacOS/MyApp
f 8f330392b30c9cbd593d MyApp.app.signed/Contents/MacOS/MyApp
d MyApp.app.signed/Contents/_CodeSignature
f ae26f0a8b94cc8fc6179 MyApp.app.signed/Contents/_CodeSignature/CodeResources
f 273cd2b4970deda577c2 MyApp.app.signed/Contents/_CodeSignature/CodeResources

$ rcodesign print-signature-info MyApp.app.signed
- path: Contents/Frameworks/libssh.4.8.8.dylib
file_size: 22544
file_sha256: 95566b60f24ce66003061a0e572d1db2e55ae0e666bba9d13a8af9b959ac2cbe
file_sha256: 40a8b05ac0eac09b118678fb3cadd62a364b5b40c0a04db953b9924d891fc3ef
entity:
mach_o:
macho_linkedit_start_offset: 16384 / 0x4000
macho_signature_start_offset: 16400 / 0x4010
macho_signature_end_offset: 16781 / 0x418d
macho_signature_end_offset: 16777 / 0x4189
macho_linkedit_end_offset: 22544 / 0x5810
macho_end_offset: 22544 / 0x5810
linkedit_signature_start_offset: 16 / 0x10
linkedit_signature_end_offset: 397 / 0x18d
linkedit_bytes_after_signature: 5763 / 0x1683
linkedit_signature_end_offset: 393 / 0x189
linkedit_bytes_after_signature: 5767 / 0x1687
signature:
superblob_length: 381 / 0x17d
superblob_length: 377 / 0x179
blob_count: 3
blobs:
- slot: CodeDirectory (0)
magic: fade0c02
length: 325
sha1: 9bb62d2fdae6551eec5eacceb650ee5181f4d326
sha256: 86e287421489643ffb6cbd07c7898b090ae6c6175d8475433d999b1c7dfb2290
length: 321
sha1: 5b19ce6b4bcd9ad15044c2066216829d95ea7ab0
sha256: 870a3508181d52e36783c1bf86010a76ad8f735165f2440f23a7c42119b4be9f
- slot: RequirementSet (2)
magic: fade0c01
length: 12
Expand All @@ -71,7 +71,7 @@ $ rcodesign print-signature-info MyApp.app.signed
code_directory:
version: '0x20400'
flags: CodeSignatureFlags(ADHOC)
identifier: libssh.4.8.8
identifier: libssh.4
digest_type: sha256
platform: 0
signed_entity_size: 16400
Expand All @@ -93,7 +93,7 @@ $ rcodesign print-signature-info MyApp.app.signed
entity: other
- path: Contents/MacOS/MyApp
file_size: 22544
file_sha256: 0b577ed98ce1915f2d294405daa4b7ae867009234a887d964005ac188b1de3d4
file_sha256: 8f330392b30c9cbd593dc332979a2a2dfcc3edfe1b3bcfd1c5e2fecb31aeca00
entity:
mach_o:
macho_linkedit_start_offset: 16384 / 0x4000
Expand All @@ -111,8 +111,8 @@ $ rcodesign print-signature-info MyApp.app.signed
- slot: CodeDirectory (0)
magic: fade0c02
length: 365
sha1: f01d0fb20b48da47daf2cbd9737a6677bfc9b1ba
sha256: ed915ac10a98c64a8faca4377d1ad5754d495edb4b0192f8653dcb31e1399b88
sha1: 98f382966f6efe2b3158f862123c12c57dc167a9
sha256: b7256b5560a1bc8c7f1e3f56d1eabc9ed248e4b97fee8e716efd5909681f95be
- slot: RequirementSet (2)
magic: fade0c01
length: 12
Expand All @@ -135,11 +135,11 @@ $ rcodesign print-signature-info MyApp.app.signed
slot_digests:
- 'Info (1): 0a5902dc8e47f490d03889d3593d17bddbf79e6c1f79494e20dd28f9459effa5'
- 'RequirementSet (2): 987920904eab650e75788c054aa0b0524e6a80bfc71aa32df8d237a61743f986'
- 'Resources (3): ae26f0a8b94cc8fc6179e9c92c6f705766a4130165208a42e5c44dcc35fd9b73'
- 'Resources (3): 273cd2b4970deda577c29fcd05055752e46144fe483371d48754d273bbccbb85'
cms: null
- path: Contents/_CodeSignature/CodeResources
file_size: 2673
file_sha256: ae26f0a8b94cc8fc6179e9c92c6f705766a4130165208a42e5c44dcc35fd9b73
file_sha256: 273cd2b4970deda577c29fcd05055752e46144fe483371d48754d273bbccbb85
entity:
bundle_code_signature_file: !ResourcesXml
- <?xml version="1.0" encoding="UTF-8"?>
Expand All @@ -154,10 +154,10 @@ $ rcodesign print-signature-info MyApp.app.signed
- ' <dict>'
- ' <key>cdhash</key>'
- ' <data>'
- ' huKHQhSJZD/7bL0Hx4mLCQrmxhc='
- ' hwo1CBgdUuNng8G/hgEKdq2Pc1E='
- ' </data>'
- ' <key>requirement</key>'
- ' <string>cdhash H"86e287421489643ffb6cbd07c7898b090ae6c617"</string>'
- ' <string>cdhash H"870a3508181d52e36783c1bf86010a76ad8f7351"</string>'
- ' </dict>'
- ' <key>Frameworks/libssh.4.dylib</key>'
- ' <dict>'
Expand Down
2 changes: 1 addition & 1 deletion apple-codesign/tests/cmd/sign-code-signature-flags.trycmd
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ $ rcodesign sign --code-signature-flags host exe.signed exe.signed.2
adding code signature flag CodeSignatureFlags(HOST) to main signing target
signing exe.signed to exe.signed.2
signing exe.signed as a Mach-O binary
setting binary identifier to exe.signed
setting binary identifier to exe
parsing Mach-O
writing Mach-O to exe.signed.2

Expand Down
2 changes: 1 addition & 1 deletion apple-codesign/tests/cmd/sign.trycmd
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ $ rcodesign print-signature-info exe.signed
$ rcodesign sign exe.signed exe.signed.2
signing exe.signed to exe.signed.2
signing exe.signed as a Mach-O binary
setting binary identifier to exe.signed
setting binary identifier to exe
parsing Mach-O
writing Mach-O to exe.signed.2

Expand Down

0 comments on commit 7d20f36

Please sign in to comment.