diff --git a/.github/workflows/rcodesign.yml b/.github/workflows/rcodesign.yml deleted file mode 100644 index f94c5b942..000000000 --- a/.github/workflows/rcodesign.yml +++ /dev/null @@ -1,20 +0,0 @@ -on: - push: - branches-ignore: - - 'ci-test' - tags-ignore: - - '**' - pull_request: - schedule: - - cron: '13 15 * * *' - workflow_dispatch: -jobs: - exes: - uses: ./.github/workflows/build-exe.yml - with: - bin: rcodesign - extra_build_args_macos: '--all-features' - extra_build_args_windows: '--all-features' - secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 79ed9b548..0ffd671f0 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -14,7 +14,6 @@ jobs: fail-fast: false matrix: dir: - - apple-codesign/docs - python-oxidized-importer/docs - pyembed/docs - pyoxy/docs diff --git a/.gitignore b/.gitignore index 34f9092fd..04330be96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ .idea/ -apple-codesign/docs/_build/ docs/_build/ -docs/apple_codesign* docs/oxidized_importer* docs/pyembed* docs/pyoxidizer* diff --git a/Cargo.lock b/Cargo.lock index 2e96968a5..1ed1d1040 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,18 +63,21 @@ checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "apple-bundles" -version = "0.14.0-pre" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c0347dcf7947f1a737f11441285b07f638b7a4b39379b65cdd0198f8d6f8a9" dependencies = [ "anyhow", "plist", "simple-file-manifest", - "tempfile", "walkdir", ] [[package]] name = "apple-codesign" -version = "0.19.0-pre" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7668a418f0d8a03c957b1a3fac8f74ed65e3db6d2b24d8fda171123297733a59" dependencies = [ "anyhow", "apple-bundles", @@ -101,7 +104,6 @@ dependencies = [ "glob", "goblin", "hex", - "indoc", "jsonwebtoken", "log", "md-5 0.10.4", @@ -136,14 +138,12 @@ dependencies = [ "tempfile", "thiserror", "tokio", - "tugger-apple", "tungstenite", "uuid", "x509", "x509-certificate", "xml-rs", "yasna", - "yubikey", "zeroize", "zip", "zip_structs", @@ -151,10 +151,12 @@ dependencies = [ [[package]] name = "apple-flat-package" -version = "0.10.0-pre" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ff1f0964ff49725ea4c0571deaa5fe0acadfd979d5a005ab137490a6e99a35" dependencies = [ "apple-xar", - "cpio-archive", + "cpio-archive 0.5.0", "flate2", "scroll", "serde", @@ -175,7 +177,9 @@ dependencies = [ [[package]] name = "apple-xar" -version = "0.10.0-pre" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6930771c2d73763b4c897865beba0b62b168f5999d84d93fa10c07f6c4f04fa4" dependencies = [ "base64", "bcder", @@ -1336,6 +1340,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27e77cfc4543efb4837662cb7cd53464ae66f0fd5c708d71e0f338b1c11d62d3" +[[package]] +name = "cpio-archive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422d3f6e51093f728814e49d4bc4820b11d7a92b4c3c99ef5a396012ddb6fafe" +dependencies = [ + "chrono", + "is_executable", + "thiserror", + "tugger-file-manifest", +] + [[package]] name = "cpio-archive" version = "0.6.0-pre" @@ -1921,7 +1937,6 @@ checksum = "85789ce7dfbd0f0624c07ef653a08bb2ebf43d3e16531361f46d36dd54334fed" dependencies = [ "der 0.6.0", "elliptic-curve", - "rfc6979", "signature", ] @@ -1967,8 +1982,6 @@ dependencies = [ "ff", "generic-array", "group", - "hkdf", - "pem-rfc7468 0.6.0", "pkcs8 0.9.0", "rand_core 0.6.4", "sec1", @@ -3413,18 +3426,6 @@ checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ "ecdsa", "elliptic-curve", - "sha2 0.10.6", -] - -[[package]] -name = "p384" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" -dependencies = [ - "ecdsa", - "elliptic-curve", - "sha2 0.10.6", ] [[package]] @@ -3474,15 +3475,6 @@ dependencies = [ "camino", ] -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest 0.10.5", -] - [[package]] name = "pbr" version = "1.0.4" @@ -3495,25 +3487,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "pcsc" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e29e4de78a433aeecd06fb5bd55a0f9fde11dc85a14c22d482972c7edc4fdc4" -dependencies = [ - "bitflags", - "pcsc-sys", -] - -[[package]] -name = "pcsc-sys" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b7bfecba2c0f1b5efb0e7caf7533ab1c295024165bcbb066231f60d33e23ea" -dependencies = [ - "pkg-config", -] - [[package]] name = "pem" version = "1.1.0" @@ -4430,17 +4403,6 @@ dependencies = [ "winreg 0.10.1", ] -[[package]] -name = "rfc6979" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c86280f057430a52f4861551b092a01b419b8eacefc7c995eacb9dc132fe32" -dependencies = [ - "crypto-bigint 0.4.8", - "hmac 0.12.1", - "zeroize", -] - [[package]] name = "ring" version = "0.16.20" @@ -4808,15 +4770,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secrecy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" -dependencies = [ - "zeroize", -] - [[package]] name = "security-framework" version = "2.7.0" @@ -5045,7 +4998,6 @@ version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb766570a2825fa972bceff0d195727876a9cdf2460ab2e52d455dc2de47fd9" dependencies = [ - "digest 0.10.5", "rand_core 0.6.4", ] @@ -5268,15 +5220,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" -[[package]] -name = "subtle-encoding" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" -dependencies = [ - "zeroize", -] - [[package]] name = "symbolic-common" version = "9.1.4" @@ -5764,6 +5707,12 @@ dependencies = [ "zip", ] +[[package]] +name = "tugger-file-manifest" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb47a7c6d38b6994490284e7572edbe226364e85cf1ab18525cfabeb0ea70be" + [[package]] name = "tugger-rust-toolchain" version = "0.12.0-pre" @@ -6399,40 +6348,6 @@ dependencies = [ "time 0.3.14", ] -[[package]] -name = "yubikey" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350b1730eb4a5734a5167bda0d400f2a28d05a9e69003c3f37163bb240f2d9c6" -dependencies = [ - "chrono", - "cookie-factory", - "der-parser", - "des 0.8.1", - "elliptic-curve", - "hmac 0.12.1", - "log", - "nom 7.1.1", - "num-bigint-dig", - "num-integer", - "num-traits", - "p256", - "p384", - "pbkdf2", - "pcsc", - "rand_core 0.6.4", - "rsa", - "secrecy", - "sha1", - "sha2 0.10.6", - "subtle", - "subtle-encoding", - "uuid", - "x509", - "x509-parser", - "zeroize", -] - [[package]] name = "zeroize" version = "1.5.7" diff --git a/Cargo.toml b/Cargo.toml index a7e4c9ca5..9462d70e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,5 @@ [workspace] members = [ - 'apple-bundles', - 'apple-codesign', - 'apple-flat-package', - 'apple-xar', 'cpio-archive', 'debian-packaging', "debian-repo-tool", diff --git a/Justfile b/Justfile index af682896c..fc4d28426 100644 --- a/Justfile +++ b/Justfile @@ -142,7 +142,6 @@ ci-run-all branch="ci-test": just ci-run oxidized_importer.yml {{branch}} just ci-run pyoxidizer.yml {{branch}} just ci-run pyoxy.yml {{branch}} - just ci-run rcodesign.yml {{branch}} just ci-run sphinx.yml {{branch}} just ci-run workspace.yml {{branch}} just ci-run workspace-python.yml {{branch}} @@ -159,9 +158,6 @@ _remote-sign-exe ref workflow run_id artifact exe_name rcodesign_branch="main": # Trigger remote code signing workflow for pyoxy executable. remote-sign-pyoxy ref run_id rcodesign_branch="main": (_remote-sign-exe ref "rcodesign.yml" run_id "exe-pyoxy-macos-universal" "pyoxy" rcodesign_branch) -# Trigger remote code signing workflow for rcodesign executable. -remote-sign-rcodesign ref run_id rcodesign_branch="main": (_remote-sign-exe ref "rcodesign.yml" run_id "exe-rcodesign-macos-universal" "rcodesign" rcodesign_branch) - # Obtain built executables from GitHub Actions. assemble-exe-artifacts exe commit dest: #!/usr/bin/env bash @@ -268,66 +264,6 @@ _release name title_name: just {{name}}-release-prepare ${COMMIT} ${TAG} just {{name}}-release-upload ${COMMIT} ${TAG} -apple-codesign-release-prepare commit tag: - #!/usr/bin/env bash - set -exo pipefail - - rm -rf dist/apple-codesign* - just assemble-exe-artifacts rcodesign {{commit}} dist/apple-codesign-artifacts - - for triple in aarch64-apple-darwin aarch64-unknown-linux-musl i686-pc-windows-msvc x86_64-apple-darwin x86_64-pc-windows-msvc x86_64-unknown-linux-musl; do - release_name=apple-codesign-{{tag}}-${triple} - source=dist/apple-codesign-artifacts/exe-rcodesign-${triple} - dest=dist/apple-codesign-stage/${release_name} - - exe=rcodesign - sign_command= - archive_action=_tar_directory - - case ${triple} in - *apple*) - sign_command="just _codesign-exe ${dest}/${exe}" - ;; - *windows*) - exe=rcodesign.exe - archive_action=_zip_directory - ;; - *) - ;; - esac - - mkdir -p ${dest} - cp -a ${source}/${exe} ${dest}/${exe} - chmod +x ${dest}/${exe} - - if [ -n "${sign_command}" ]; then - ${sign_command} - fi - - cargo run --bin pyoxidizer -- rust-project-licensing \ - --system-rust \ - --target-triple ${triple} \ - --all-features \ - --unified-license \ - apple-codesign > ${dest}/COPYING - - mkdir -p dist/apple-codesign - - just ${archive_action} dist/apple-codesign-stage ${release_name} dist/apple-codesign - done - - # Create universal binary. - just _release_universal_binary apple-codesign {{tag}} rcodesign - just _tar_directory dist/apple-codesign-stage apple-codesign-{{tag}}-macos-universal dist/apple-codesign - - just _create_shasums dist/apple-codesign - -apple-codesign-release-upload commit tag: - just _upload_release apple-codesign 'Apple Codesign' {{commit}} {{tag}} - -apple-codesign-release: - just _release apple-codesign 'Apple Codesign' - # Prepare PyOxy release artifacts. pyoxy-release-prepare commit tag: #!/usr/bin/env bash diff --git a/apple-bundles/Cargo.toml b/apple-bundles/Cargo.toml deleted file mode 100644 index 52a1cb5f2..000000000 --- a/apple-bundles/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "apple-bundles" -version = "0.14.0-pre" -authors = ["Gregory Szorc "] -edition = "2021" -license = "MPL-2.0" -description = "Interface with Apple bundle primitives" -keywords = ["apple", "macos", "bundle", "appbundle"] -homepage = "https://github.com/indygreg/PyOxidizer" -repository = "https://github.com/indygreg/PyOxidizer.git" -readme = "README.md" - -[dependencies] -anyhow = "1.0" -plist = "1.2" -simple-file-manifest = "0.11" -walkdir = "2.3" - -[dev-dependencies] -tempfile = "3.3" diff --git a/apple-bundles/README.md b/apple-bundles/README.md deleted file mode 100644 index a414096ba..000000000 --- a/apple-bundles/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# apple-bundles - -`apple-bundles` is a library crate implementing functionality related -to Apple *bundles*, a foundational primitive in Apple operating systems for -encapsulating code and resources. (The ``.app`` *directories* in -``/Applications`` are *application bundles*, for example.) - -`apple-bundles` is part of the -[PyOxidizer](https://github.com/indygreg/PyOxidizer.git) project and this -crate is developed in that repository. - -While this crate is developed as part of a larger project, modifications -to support its use outside of its primary use case are very much welcome! diff --git a/apple-bundles/src/directory_bundle.rs b/apple-bundles/src/directory_bundle.rs deleted file mode 100644 index 0b91a8bdf..000000000 --- a/apple-bundles/src/directory_bundle.rs +++ /dev/null @@ -1,660 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Bundles backed by a directory. - -use { - crate::BundlePackageType, - anyhow::{anyhow, Context, Result}, - simple_file_manifest::{is_executable, FileEntry, FileManifest}, - std::{ - collections::HashSet, - path::{Path, PathBuf}, - }, -}; - -/// An Apple bundle backed by a filesystem/directory. -/// -/// Instances represent a type-agnostic bundle (macOS application bundle, iOS -/// application bundle, framework bundles, etc). -#[derive(Clone, Debug)] -pub struct DirectoryBundle { - /// Root directory of this bundle. - root: PathBuf, - - /// Name of the root directory. - root_name: String, - - /// Whether the bundle is shallow. - /// - /// If false, content is in a `Contents/` sub-directory. - shallow: bool, - - /// The type of this bundle. - package_type: BundlePackageType, - - /// Parsed `Info.plist` file. - info_plist: plist::Dictionary, -} - -impl DirectoryBundle { - /// Open an existing bundle from a filesystem path. - /// - /// The specified path should be the root directory of the bundle. - /// - /// This will validate that the directory is a bundle and error if not. - /// Validation is limited to locating an `Info.plist` file, which is - /// required for all bundle types. - pub fn new_from_path(directory: &Path) -> Result { - if !directory.is_dir() { - return Err(anyhow!("{} is not a directory", directory.display())); - } - - let root_name = directory - .file_name() - .ok_or_else(|| anyhow!("unable to resolve root directory name"))? - .to_string_lossy() - .to_string(); - - let contents = directory.join("Contents"); - let shallow = !contents.is_dir(); - - let app_plist = if shallow { - directory.join("Info.plist") - } else { - contents.join("Info.plist") - }; - - let framework_plist = directory.join("Resources").join("Info.plist"); - - // Shallow bundles make it very easy to mis-identify a directory as a bundle. - // The the following iOS app bundle directory structure: - // - // MyApp.app - // MyApp - // Info.plist - // - // And take this framework directory structure: - // - // MyFramework.framework - // MyFramework -> Versions/Current/MyFramework - // Resources/ - // Info.plist - // - // Depending on how we probe the directories, `MyFramework.framework/Resources` - // looks like a shallow app bundle! - // - // Frameworks are also an interesting use case. Frameworks often have a `Versions/` - // where there may exist multiple versions of the framework. Each directory under - // `Versions` is itself a valid framework! - // - // MyFramework.framework - // MyFramework -> Versions/Current/MyFramework - // Resources -> Versions/Current/Resources - // Versions/ - // A/ - // MyFramework - // Resources/ - // Info.plist - // Current -> A - - // Frameworks must have a `Resources/Info.plist`. It is tempting to look for the - // `.framework` extension as well. However - let (package_type, info_plist_path) = if framework_plist.is_file() { - (BundlePackageType::Framework, framework_plist) - } else if app_plist.is_file() { - if root_name.ends_with(".app") { - (BundlePackageType::App, app_plist) - } else { - // This can definitely lead to false positives. - (BundlePackageType::Bundle, app_plist) - } - } else { - return Err(anyhow!("Info.plist not found; not a valid bundle")); - }; - - let info_plist_data = std::fs::read(&info_plist_path)?; - let cursor = std::io::Cursor::new(info_plist_data); - let value = plist::Value::from_reader(cursor).context("parsing Info.plist")?; - let info_plist = value - .into_dictionary() - .ok_or_else(|| anyhow!("{} is not a dictionary", info_plist_path.display()))?; - - Ok(Self { - root: directory.to_path_buf(), - root_name, - shallow, - package_type, - info_plist, - }) - } - - /// Resolve the absolute path to a file in the bundle. - pub fn resolve_path(&self, path: impl AsRef) -> PathBuf { - if self.shallow { - self.root.join(path.as_ref()) - } else { - self.root.join("Contents").join(path.as_ref()) - } - } - - /// The root directory of this bundle. - pub fn root_dir(&self) -> &Path { - &self.root - } - - /// The on-disk name of this bundle. - /// - /// This is effectively the directory name of the bundle. Contains the `.app`, - /// `.framework`, etc suffix. - pub fn name(&self) -> &str { - &self.root_name - } - - /// Whether this is a shallow bundle. - /// - /// If false, content is likely in a `Contents` directory. - pub fn shallow(&self) -> bool { - self.shallow - } - - /// Obtain the path to the `Info.plist` file. - pub fn info_plist_path(&self) -> PathBuf { - match self.package_type { - BundlePackageType::App | BundlePackageType::Bundle => self.resolve_path("Info.plist"), - BundlePackageType::Framework => self.root.join("Resources").join("Info.plist"), - } - } - - /// Obtain the parsed `Info.plist` file. - pub fn info_plist(&self) -> &plist::Dictionary { - &self.info_plist - } - - /// Obtain an `Info.plist` key as a `String`. - /// - /// Will return `None` if the specified key doesn't exist. Errors if the key value - /// is not a string. - pub fn info_plist_key_string(&self, key: &str) -> Result> { - if let Some(value) = self.info_plist.get(key) { - Ok(Some( - value - .as_string() - .ok_or_else(|| anyhow!("key {} is not a string", key))? - .to_string(), - )) - } else { - Ok(None) - } - } - - /// Obtain the type of bundle. - pub fn package_type(&self) -> BundlePackageType { - self.package_type - } - - /// Obtain the bundle display name. - /// - /// This retrieves the value of `CFBundleDisplayName` from the `Info.plist`. - pub fn display_name(&self) -> Result> { - self.info_plist_key_string("CFBundleDisplayName") - } - - /// Obtain the bundle identifier. - /// - /// This retrieves `CFBundleIdentifier` from the `Info.plist`. - pub fn identifier(&self) -> Result> { - self.info_plist_key_string("CFBundleIdentifier") - } - - /// Obtain the bundle version string. - /// - /// This retrieves `CFBundleVersion` from the `Info.plist`. - pub fn version(&self) -> Result> { - self.info_plist_key_string("CFBundleVersion") - } - - /// Obtain the name of the bundle's main executable file. - /// - /// This retrieves `CFBundleExecutable` from the `Info.plist`. - pub fn main_executable(&self) -> Result> { - self.info_plist_key_string("CFBundleExecutable") - } - - /// Obtain filenames of bundle icon files. - /// - /// This retrieves `CFBundleIconFiles` from the `Info.plist`. - pub fn icon_files(&self) -> Result>> { - if let Some(value) = self.info_plist.get("CFBundleIconFiles") { - let values = value - .as_array() - .ok_or_else(|| anyhow!("CFBundleIconFiles not an array"))?; - - Ok(Some( - values - .iter() - .map(|x| { - Ok(x.as_string() - .ok_or_else(|| anyhow!("CFBundleIconFiles value not a string"))? - .to_string()) - }) - .collect::>>()?, - )) - } else { - Ok(None) - } - } - - /// Obtain all files within this bundle. - /// - /// The iteration order is deterministic. - /// - /// `traverse_nested` defines whether to traverse into nested bundles. - pub fn files(&self, traverse_nested: bool) -> Result>> { - let nested_dirs = self - .nested_bundles(true)? - .into_iter() - .map(|(_, bundle)| bundle.root_dir().to_path_buf()) - .collect::>(); - - Ok(walkdir::WalkDir::new(&self.root) - .sort_by_file_name() - .into_iter() - .map(|entry| { - let entry = entry?; - - Ok(entry.path().to_path_buf()) - }) - .collect::>>()? - .into_iter() - .filter_map(|path| { - // This path is part of a known nested bundle and we're not in traversal mode. - // Stop immediately. - if !traverse_nested - && nested_dirs - .iter() - .any(|prefix| path.strip_prefix(prefix).is_ok()) - { - None - // Symlinks are emitted as files, even if they point to a directory. It is - // up to callers to handle symlinks correctly. - } else if path.is_symlink() || !path.is_dir() { - Some(DirectoryBundleFile::new(self, path)) - } else { - None - } - }) - .collect::>()) - } - - /// Obtain all files in this bundle as a [FileManifest]. - pub fn files_manifest(&self, traverse_nested: bool) -> Result { - let mut m = FileManifest::default(); - - for f in self.files(traverse_nested)? { - m.add_file_entry(f.relative_path(), f.as_file_entry()?)?; - } - - Ok(m) - } - - /// Obtain all nested bundles within this one. - /// - /// This walks the directory tree for directories that can be parsed - /// as bundles. - /// - /// If `descend` is true, we will descend into nested bundles and recursively emit nested - /// bundles. Otherwise we stop traversal once a bundle is encountered. - pub fn nested_bundles(&self, descend: bool) -> Result> { - let mut bundles = vec![]; - - let mut poisoned_prefixes = HashSet::new(); - - for entry in walkdir::WalkDir::new(&self.root).sort_by_file_name() { - let entry = entry?; - - let path = entry.path(); - - // Ignore self. - if path == self.root { - continue; - } - - // A nested bundle must be a directory. - if !path.is_dir() || path.is_symlink() { - continue; - } - - // This directory is inside a directory that has already been searched for - // nested bundles. So ignore. - if poisoned_prefixes - .iter() - .any(|prefix| path.strip_prefix(prefix).is_ok()) - { - continue; - } - - let root_relative = path.strip_prefix(&self.root)?.to_string_lossy(); - - // Some bundle types have known child directories that themselves - // can't be bundles. Exclude those from the search. - match self.package_type { - BundlePackageType::Framework => { - // Resources and Versions are known directories under frameworks. - // They can't be bundles. - if matches!(root_relative.as_ref(), "Resources" | "Versions") { - continue; - } - } - _ => { - if root_relative == "Contents" { - continue; - } - } - } - - // If we got here, test for bundle-ness by using our constructor. - let bundle = match Self::new_from_path(path) { - Ok(bundle) => bundle, - Err(_) => { - continue; - } - }; - - bundles.push((root_relative.to_string(), bundle.clone())); - - if descend { - for (path, nested) in bundle.nested_bundles(true)? { - bundles.push((format!("{}/{}", root_relative, path), nested)); - } - } - - poisoned_prefixes.insert(path.to_path_buf()); - } - - Ok(bundles) - } - - /// Resolve the versions present within a framework. - /// - /// Does not emit versions that are symlinks. - pub fn framework_versions(&self) -> Result> { - if self.package_type != BundlePackageType::Framework { - return Ok(vec![]); - } - - let mut res = vec![]; - - for entry in std::fs::read_dir(self.root.join("Versions"))? { - let entry = entry?; - let metadata = entry.metadata()?; - - if metadata.is_dir() && !metadata.is_symlink() { - res.push(entry.file_name().to_string_lossy().to_string()); - } - } - - // Be deterministic. - res.sort(); - - Ok(res) - } - - /// Whether this bundle is a version within a framework bundle. - /// - /// This is true if we are a framework bundle under a `Versions` directory. - pub fn is_framework_version(&self) -> bool { - if self.package_type == BundlePackageType::Framework { - if let Some(parent) = self.root.parent() { - if let Some(file_name) = parent.file_name() { - file_name == "Versions" - } else { - false - } - } else { - false - } - } else { - false - } - } -} - -/// Represents a file in a [DirectoryBundle]. -pub struct DirectoryBundleFile<'a> { - bundle: &'a DirectoryBundle, - absolute_path: PathBuf, - relative_path: PathBuf, -} - -impl<'a> DirectoryBundleFile<'a> { - fn new(bundle: &'a DirectoryBundle, absolute_path: PathBuf) -> Self { - let relative_path = absolute_path - .strip_prefix(&bundle.root) - .expect("path prefix strip should have worked") - .to_path_buf(); - - Self { - bundle, - absolute_path, - relative_path, - } - } - - /// Absolute path to this file. - pub fn absolute_path(&self) -> &Path { - &self.absolute_path - } - - /// Relative path within the bundle to this file. - pub fn relative_path(&self) -> &Path { - &self.relative_path - } - - /// Whether this is the `Info.plist` file. - pub fn is_info_plist(&self) -> bool { - self.absolute_path == self.bundle.info_plist_path() - } - - /// Whether this is the main executable for the bundle. - pub fn is_main_executable(&self) -> Result { - if let Some(main) = self.bundle.main_executable()? { - if self.bundle.shallow() { - Ok(self.absolute_path == self.bundle.resolve_path(main)) - } else { - Ok(self.absolute_path == self.bundle.resolve_path(format!("MacOS/{}", main))) - } - } else { - Ok(false) - } - } - - /// Whether this is the `_CodeSignature/CodeResources` XML plist file. - pub fn is_code_resources_xml_plist(&self) -> bool { - self.absolute_path == self.bundle.resolve_path("_CodeSignature/CodeResources") - } - - /// Whether this is the `CodeResources` file holding the notarization ticket. - pub fn is_notarization_ticket(&self) -> bool { - self.absolute_path == self.bundle.resolve_path("CodeResources") - } - - /// Whether this file is in the code signature directory. - pub fn is_in_code_signature_directory(&self) -> bool { - let prefix = self.bundle.resolve_path("_CodeSignature"); - - self.absolute_path.starts_with(&prefix) - } - - /// Obtain the symlink target for this file. - /// - /// If `None`, the file is not a symlink. - pub fn symlink_target(&self) -> Result> { - let metadata = self.metadata()?; - - if metadata.file_type().is_symlink() { - Ok(Some(std::fs::read_link(&self.absolute_path)?)) - } else { - Ok(None) - } - } - - /// Obtain metadata for this file. - /// - /// Does not follow symlinks. - pub fn metadata(&self) -> Result { - Ok(self.absolute_path.symlink_metadata()?) - } - - /// Convert this instance to a [FileEntry]. - pub fn as_file_entry(&self) -> Result { - let metadata = self.metadata()?; - - let mut entry = FileEntry::new_from_path(self.absolute_path(), is_executable(&metadata)); - - if let Some(target) = self.symlink_target()? { - entry.set_link_target(target); - } - - Ok(entry) - } -} - -#[cfg(test)] -mod test { - use {super::*, std::fs::create_dir_all}; - - fn temp_dir() -> Result<(tempfile::TempDir, PathBuf)> { - let td = tempfile::Builder::new() - .prefix("apple-bundles-") - .tempdir()?; - let path = td.path().to_path_buf(); - - Ok((td, path)) - } - - #[test] - fn app_simple() -> Result<()> { - let (_temp, td) = temp_dir()?; - - // Empty directory fails. - let root = td.join("MyApp.app"); - create_dir_all(&root)?; - assert!(DirectoryBundle::new_from_path(&root).is_err()); - - // Empty Contents/ fails. - let contents = root.join("Contents"); - create_dir_all(&contents)?; - assert!(DirectoryBundle::new_from_path(&root).is_err()); - - // Empty Info.plist fails. - let plist_path = contents.join("Info.plist"); - std::fs::write(&plist_path, &[])?; - assert!(DirectoryBundle::new_from_path(&root).is_err()); - - // Empty plist dictionary works. - let empty = plist::Value::from(plist::Dictionary::new()); - empty.to_file_xml(&plist_path)?; - let bundle = DirectoryBundle::new_from_path(&root)?; - - assert_eq!(bundle.package_type, BundlePackageType::App); - assert_eq!(bundle.name(), "MyApp.app"); - assert!(!bundle.shallow()); - assert_eq!(bundle.identifier()?, None); - assert!(bundle.nested_bundles(true)?.is_empty()); - - Ok(()) - } - - #[test] - fn framework() -> Result<()> { - let (_temp, td) = temp_dir()?; - - // Empty directory fails. - let root = td.join("MyFramework.framework"); - create_dir_all(&root)?; - assert!(DirectoryBundle::new_from_path(&root).is_err()); - - // Empty Resources/ fails. - let resources = root.join("Resources"); - create_dir_all(&resources)?; - assert!(DirectoryBundle::new_from_path(&root).is_err()); - - // Empty Info.plist file fails. - let plist_path = resources.join("Info.plist"); - std::fs::write(&plist_path, &[])?; - assert!(DirectoryBundle::new_from_path(&root).is_err()); - - // Empty plist dictionary works. - let empty = plist::Value::from(plist::Dictionary::new()); - empty.to_file_xml(&plist_path)?; - let bundle = DirectoryBundle::new_from_path(&root)?; - - assert_eq!(bundle.package_type, BundlePackageType::Framework); - assert_eq!(bundle.name(), "MyFramework.framework"); - assert!(bundle.shallow()); - assert_eq!(bundle.identifier()?, None); - assert!(bundle.nested_bundles(true)?.is_empty()); - - Ok(()) - } - - #[test] - fn framework_in_app() -> Result<()> { - let (_temp, td) = temp_dir()?; - - let root = td.join("MyApp.app"); - let contents = root.join("Contents"); - create_dir_all(&contents)?; - - let app_info_plist = contents.join("Info.plist"); - let empty = plist::Value::Dictionary(plist::Dictionary::new()); - empty.to_file_xml(&app_info_plist)?; - - let frameworks = contents.join("Frameworks"); - let framework = frameworks.join("MyFramework.framework"); - let resources = framework.join("Resources"); - create_dir_all(&resources)?; - let versions = framework.join("Versions"); - create_dir_all(&versions)?; - let framework_info_plist = resources.join("Info.plist"); - empty.to_file_xml(&framework_info_plist)?; - let framework_resource_file_root = resources.join("root00.txt"); - std::fs::write(&framework_resource_file_root, &[])?; - - let framework_child = resources.join("child_dir"); - create_dir_all(&framework_child)?; - let framework_resource_file_child = framework_child.join("child00.txt"); - std::fs::write(&framework_resource_file_child, &[])?; - - let a_resources = versions.join("A").join("Resources"); - create_dir_all(&a_resources)?; - let b_resources = versions.join("B").join("Resources"); - create_dir_all(&b_resources)?; - let a_plist = a_resources.join("Info.plist"); - empty.to_file_xml(&a_plist)?; - let b_plist = b_resources.join("Info.plist"); - empty.to_file_xml(&b_plist)?; - - let bundle = DirectoryBundle::new_from_path(&root)?; - - let nested = bundle.nested_bundles(true)?; - assert_eq!(nested.len(), 3); - assert_eq!( - nested - .iter() - .map(|x| x.0.replace("\\", "/")) - .collect::>(), - vec![ - "Contents/Frameworks/MyFramework.framework", - "Contents/Frameworks/MyFramework.framework/Versions/A", - "Contents/Frameworks/MyFramework.framework/Versions/B", - ] - ); - - assert_eq!(nested[0].1.framework_versions()?, vec!["A", "B"]); - - Ok(()) - } -} diff --git a/apple-bundles/src/lib.rs b/apple-bundles/src/lib.rs deleted file mode 100644 index b4d5dd97b..000000000 --- a/apple-bundles/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -mod directory_bundle; -pub use directory_bundle::*; -mod macos_application_bundle; -pub use macos_application_bundle::*; - -/// Denotes the type of a bundle. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum BundlePackageType { - /// Application bundle. - App, - /// Framework bundle. - Framework, - /// Generic bundle. - Bundle, -} - -impl ToString for BundlePackageType { - fn to_string(&self) -> String { - match self { - Self::App => "APPL", - Self::Framework => "FMWK", - Self::Bundle => "BNDL", - } - .to_string() - } -} diff --git a/apple-bundles/src/macos_application_bundle.rs b/apple-bundles/src/macos_application_bundle.rs deleted file mode 100644 index 2461c44d9..000000000 --- a/apple-bundles/src/macos_application_bundle.rs +++ /dev/null @@ -1,458 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -/*! macOS Application Bundles - -See https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1 -for documentation of the macOS Application Bundle format. -*/ - -use { - crate::BundlePackageType, - anyhow::{anyhow, Context, Result}, - simple_file_manifest::{FileEntry, FileManifest, FileManifestError}, - std::path::{Path, PathBuf}, -}; - -/// Primitive used to iteratively construct a macOS Application Bundle. -/// -/// Under the hood, the builder maintains a list of files that will constitute -/// the final, materialized bundle. There is a low-level `add_file()` API for -/// adding a file at an explicit path within the bundle. This gives you full -/// control over the content of the bundle. -/// -/// There are also a number of high-level APIs for performing common tasks, such -/// as defining required bundle metadata for the `Contents/Info.plist` file and -/// adding files to specific locations. There are even APIs for performing -/// lower-level manipulation of certain files, such as adding keys to the -/// `Content/Info.plist` file. -/// -/// Apple's documentation on the -/// [bundle format](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1) -/// is very comprehensive and can answer many questions. The most important -/// takeaways are: -/// -/// 1. The `Contents/Info.plist` must contain some required keys defining the -/// bundle. Call `set_info_plist_required_keys()` to ensure these are -/// defined. -/// 2. There must be an executable file in the `Contents/MacOS` directory. Add -/// one via `add_file_macos()`. -/// -/// This type attempts to prevent some misuse (such as validating `Info.plist` -/// content) but it cannot prevent all misconfigurations. -/// -/// # Examples -/// -/// ``` -/// use apple_bundles::MacOsApplicationBundleBuilder; -/// use simple_file_manifest::FileEntry; -/// -/// # fn main() -> anyhow::Result<()> { -/// let mut builder = MacOsApplicationBundleBuilder::new("MyProgram")?; -/// -/// // Populate some required keys in Contents/Info.plist. -/// builder.set_info_plist_required_keys("My Program", "com.example.my_program", "0.1", "mypg", "MyProgram")?; -/// -/// // Add an executable file providing our main application. -/// builder.add_file_macos("MyProgram", FileEntry::new_from_data(b"#!/bin/sh\necho 'hello world'\n".to_vec(), true))?; -/// # Ok(()) -/// # } -/// ``` -#[derive(Clone, Debug)] -pub struct MacOsApplicationBundleBuilder { - /// Files constituting the application bundle. - files: FileManifest, -} - -impl MacOsApplicationBundleBuilder { - /// Create a new macOS Application Bundle builder. - /// - /// The bundle will be populated with a skeleton `Contents/Info.plist` file - /// defining the bundle name passed. - pub fn new(bundle_name: impl ToString) -> Result { - let mut instance = Self { - files: FileManifest::default(), - }; - - instance - .set_info_plist_key("CFBundleName", bundle_name.to_string()) - .context("setting CFBundleName")?; - - // This is an application bundle, so CFBundlePackageType is constant. - instance - .set_info_plist_key("CFBundlePackageType", BundlePackageType::App.to_string()) - .context("setting CFBundlePackageType")?; - - Ok(instance) - } - - /// Obtain the raw FileManifest backing this builder. - pub fn files(&self) -> &FileManifest { - &self.files - } - - /// Obtain the name of the bundle. - /// - /// This will parse the stored `Contents/Info.plist` and return the - /// value of the `CFBundleName` key. - /// - /// This will error if the stored `Info.plist` is malformed, is missing - /// a key, or the key has the wrong type. Errors should only happen if - /// the file was explicitly stored or the value of this key was explicitly - /// defined to the wrong type. - pub fn bundle_name(&self) -> Result { - Ok(self - .get_info_plist_key("CFBundleName") - .context("resolving CFBundleName")? - .ok_or_else(|| anyhow!("CFBundleName key not defined"))? - .as_string() - .ok_or_else(|| anyhow!("CFBundleName is not a string"))? - .to_string()) - } - - /// Obtain the parsed content of the `Contents/Info.plist` file. - /// - /// Returns `Some(T)` if a `Contents/Info.plist` is defined or `None` if - /// not. - /// - /// Returns `Err` if the file content could not be resolved or fails to parse - /// as a plist dictionary. - pub fn info_plist(&self) -> Result> { - if let Some(entry) = self.files.get("Contents/Info.plist") { - let data = entry.resolve_content().context("resolving file content")?; - let cursor = std::io::Cursor::new(data); - - let value = plist::Value::from_reader_xml(cursor).context("parsing plist")?; - - if let Some(dict) = value.into_dictionary() { - Ok(Some(dict)) - } else { - Err(anyhow!("parsed plist is not a dictionary")) - } - } else { - Ok(None) - } - } - - /// Add a file to this application bundle. - /// - /// The path specified will be added without any checking, replacing - /// an existing file at that path, if present. - pub fn add_file( - &mut self, - path: impl AsRef, - entry: impl Into, - ) -> Result<(), FileManifestError> { - self.files.add_file_entry(path, entry) - } - - /// Set the content of `Contents/Info.plist` using a `plist::Dictionary`. - /// - /// This allows you to define the `Info.plist` file with some validation - /// since it goes through a plist serialization API, which should produce a - /// valid plist file (although the contents of the plist may be invalid - /// for an application bundle). - pub fn set_info_plist_from_dictionary(&mut self, value: plist::Dictionary) -> Result<()> { - let mut data: Vec = vec![]; - - let value = plist::Value::from(value); - - value - .to_writer_xml(&mut data) - .context("serializing plist dictionary to XML")?; - - Ok(self.add_file("Contents/Info.plist", data)?) - } - - /// Obtain the value of a key in the `Contents/Info.plist` file. - /// - /// Returns `Some(Value)` if the key exists, `None` otherwise. - /// - /// May error if the stored `Contents/Info.plist` file is malformed. - pub fn get_info_plist_key(&self, key: &str) -> Result> { - Ok( - if let Some(dict) = self.info_plist().context("parsing Info.plist")? { - dict.get(key).cloned() - } else { - None - }, - ) - } - - /// Set the value of a key in the `Contents/Info.plist` file. - /// - /// This API can be used to iteratively build up the `Info.plist` file by - /// setting keys in it. - /// - /// If an existing key is replaced, `Some(Value)` will be returned. - pub fn set_info_plist_key( - &mut self, - key: impl ToString, - value: impl Into, - ) -> Result> { - let mut dict = if let Some(dict) = self.info_plist().context("retrieving Info.plist")? { - dict - } else { - plist::Dictionary::new() - }; - - let old = dict.insert(key.to_string(), value.into()); - - self.set_info_plist_from_dictionary(dict) - .context("replacing Info.plist dictionary")?; - - Ok(old) - } - - /// Defines required keys in the `Contents/Info.plist` file. - /// - /// The following keys are set: - /// - /// `display_name` sets `CFBundleDisplayName`, the bundle display name. - /// `identifier` sets `CFBundleIdentifier`, the bundle identifier. - /// `version` sets `CFBundleVersion`, the bundle version string. - /// `signature` sets `CFBundleSignature`, the bundle creator OS type code. - /// `executable` sets `CFBundleExecutable`, the name of the main executable file. - pub fn set_info_plist_required_keys( - &mut self, - display_name: impl ToString, - identifier: impl ToString, - version: impl ToString, - signature: impl ToString, - executable: impl ToString, - ) -> Result<()> { - let signature = signature.to_string(); - - if signature.len() != 4 { - return Err(anyhow!( - "signature must be exactly 4 characters; got {}", - signature - )); - } - - self.set_info_plist_key("CFBundleDisplayName", display_name.to_string()) - .context("setting CFBundleDisplayName")?; - self.set_info_plist_key("CFBundleIdentifier", identifier.to_string()) - .context("setting CFBundleIdentifier")?; - self.set_info_plist_key("CFBundleVersion", version.to_string()) - .context("setting CFBundleVersion")?; - self.set_info_plist_key("CFBundleSignature", signature) - .context("setting CFBundleSignature")?; - self.set_info_plist_key("CFBundleExecutable", executable.to_string()) - .context("setting CFBundleExecutable")?; - - Ok(()) - } - - /// Add the icon for the bundle. - /// - /// This will materialize the passed raw image data (can be multiple formats) - /// into the `Contents/Resources/.icns` file. - pub fn add_icon(&mut self, data: impl Into) -> Result<()> { - Ok(self.add_file_resources( - format!( - "{}.icns", - self.bundle_name().context("resolving bundle name")? - ), - data, - )?) - } - - /// Add a file to the `Contents/MacOS/` directory. - /// - /// The passed path will be prefixed with `Contents/MacOS/`. - pub fn add_file_macos( - &mut self, - path: impl AsRef, - entry: impl Into, - ) -> Result<(), FileManifestError> { - self.add_file(PathBuf::from("Contents/MacOS").join(path), entry) - } - - /// Add a file to the `Contents/Resources/` directory. - /// - /// The passed path will be prefixed with `Contents/Resources/` - pub fn add_file_resources( - &mut self, - path: impl AsRef, - entry: impl Into, - ) -> Result<(), FileManifestError> { - self.add_file(PathBuf::from("Contents/Resources").join(path), entry) - } - - /// Add a localized resources file. - /// - /// This is a convenience wrapper to `add_file_resources()` which automatically - /// places the file in the appropriate directory given the name of a locale. - pub fn add_localized_resources_file( - &mut self, - locale: impl ToString, - path: impl AsRef, - entry: impl Into, - ) -> Result<(), FileManifestError> { - self.add_file_resources( - PathBuf::from(format!("{}.lproj", locale.to_string())).join(path), - entry, - ) - } - - /// Add a file to the `Contents/Frameworks/` directory. - /// - /// The passed path will be prefixed with `Contents/Frameworks/`. - pub fn add_file_frameworks( - &mut self, - path: impl AsRef, - entry: impl Into, - ) -> Result<(), FileManifestError> { - self.add_file(PathBuf::from("Contents/Frameworks").join(path), entry) - } - - /// Add a file to the `Contents/Plugins/` directory. - /// - /// The passed path will be prefixed with `Contents/Plugins/`. - pub fn add_file_plugins( - &mut self, - path: impl AsRef, - entry: impl Into, - ) -> Result<(), FileManifestError> { - self.add_file(PathBuf::from("Contents/Plugins").join(path), entry) - } - - /// Add a file to the `Contents/SharedSupport/` directory. - /// - /// The passed path will be prefixed with `Contents/SharedSupport/`. - pub fn add_file_shared_support( - &mut self, - path: impl AsRef, - entry: impl Into, - ) -> Result<(), FileManifestError> { - self.add_file(PathBuf::from("Contents/SharedSupport").join(path), entry) - } - - /// Materialize this bundle to the specified directory. - /// - /// All files comprising this bundle will be written to a directory named - /// `.app` in the directory specified. The path of this directory - /// will be returned. - /// - /// If the destination bundle directory exists, existing files will be - /// overwritten. Files already in the destination not defined in this - /// builder will not be touched. - pub fn materialize_bundle(&self, dest_dir: impl AsRef) -> Result { - let bundle_name = self.bundle_name().context("resolving bundle name")?; - let bundle_dir = dest_dir.as_ref().join(format!("{}.app", bundle_name)); - - self.files - .materialize_files(&bundle_dir) - .context("materializing FileManifest")?; - - Ok(bundle_dir) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn new_plist() -> Result<()> { - let builder = MacOsApplicationBundleBuilder::new("MyProgram")?; - - let entries = builder.files().iter_entries().collect::>(); - assert_eq!(entries.len(), 1); - assert_eq!(entries[0].0, &PathBuf::from("Contents/Info.plist")); - - let mut dict = plist::Dictionary::new(); - dict.insert("CFBundleName".to_string(), "MyProgram".to_string().into()); - dict.insert("CFBundlePackageType".to_string(), "APPL".to_string().into()); - - assert_eq!(builder.info_plist()?, Some(dict)); - assert!(String::from_utf8(entries[0].1.resolve_content()?)? - .starts_with("")); - - Ok(()) - } - - #[test] - fn plist_set() -> Result<()> { - let mut builder = MacOsApplicationBundleBuilder::new("MyProgram")?; - - builder.set_info_plist_required_keys( - "My Program", - "com.example.my_program", - "0.1", - "mypg", - "MyProgram", - )?; - - let dict = builder.info_plist()?.unwrap(); - assert_eq!( - dict.get("CFBundleDisplayName"), - Some(&plist::Value::from("My Program")) - ); - assert_eq!( - dict.get("CFBundleIdentifier"), - Some(&plist::Value::from("com.example.my_program")) - ); - assert_eq!( - dict.get("CFBundleVersion"), - Some(&plist::Value::from("0.1")) - ); - assert_eq!( - dict.get("CFBundleSignature"), - Some(&plist::Value::from("mypg")) - ); - assert_eq!( - dict.get("CFBundleExecutable"), - Some(&plist::Value::from("MyProgram")) - ); - - Ok(()) - } - - #[test] - fn add_icon() -> Result<()> { - let mut builder = MacOsApplicationBundleBuilder::new("MyProgram")?; - - builder.add_icon(vec![42])?; - - let entries = builder.files.iter_entries().collect::>(); - assert_eq!(entries.len(), 2); - assert_eq!( - entries[1].0, - &PathBuf::from("Contents/Resources/MyProgram.icns") - ); - - Ok(()) - } - - #[test] - fn add_file_macos() -> Result<()> { - let mut builder = MacOsApplicationBundleBuilder::new("MyProgram")?; - - builder.add_file_macos("MyProgram", FileEntry::new_from_data(vec![42], true))?; - - let entries = builder.files.iter_entries().collect::>(); - assert_eq!(entries.len(), 2); - assert_eq!(entries[1].0, &PathBuf::from("Contents/MacOS/MyProgram")); - - Ok(()) - } - - #[test] - fn add_localized_resources_file() -> Result<()> { - let mut builder = MacOsApplicationBundleBuilder::new("MyProgram")?; - - builder.add_localized_resources_file("it", "resource", vec![42])?; - - let entries = builder.files.iter_entries().collect::>(); - assert_eq!(entries.len(), 2); - assert_eq!( - entries[1].0, - &PathBuf::from("Contents/Resources/it.lproj/resource") - ); - - Ok(()) - } -} diff --git a/apple-codesign/CHANGELOG.rst b/apple-codesign/CHANGELOG.rst deleted file mode 100644 index bb88ce82f..000000000 --- a/apple-codesign/CHANGELOG.rst +++ /dev/null @@ -1,357 +0,0 @@ -======================== -`apple-codesign` History -======================== - -0.18.0 -====== - -(Released 2022-09-17) - -* Mach-O digesting code now digests file-level data without looking at segment - boundaries. This fixes a bug where we were computing the incorrect digests when - Mach-O segments weren't aligned at 4096 byte boundaries. (Go binaries commonly - don't have 4k aligned segment boundaries.) (#634) -* Optimizations to computing cryptographic digests of binaries. We eliminate a - a redundant digest that was used to compute the final size of the code digests. - The ``rayon`` crate is now used to perform digests in parallel, yielding a - ~linear speedup with the number of CPUs available. -* (API) ``app_store_connect`` module has been split up into multiple modules - to facilitate better grouping. -* (API) Various changes for upgrades of crates related to cryptography. -* der crate upgraded from 0.5 to 0.6. -* elliptic-curve crate upgraded from 0.11 to 0.12. -* oid-registry crate upgraded from 0.5 to 0.6. -* p256 crate upgraded from 0.10 to 0.11. -* pkcs1 crate upgraded from 0.3 to 0.4. -* pkcs8 crate upgraded from 0.8 to 0.9. -* spki crate upgraded from 0.5 to 0.6. -* yubikey crate upgraded from 0.4 to 0.6. -* (API) The ``code_hash`` module had its content folded into the new function - ``MachOBinary::code_digests()``. - -0.17.0 -====== - -(Released 2022-08-07) - -* **Major feature**: Notarization is now implemented in Rust and no longer - requires Apple's *Transporter* application. Going forward, you only need - the ``rcodesign`` executable (or this crate embedded as a library) and an - App Store Connect API Key to notarize. Major thanks to Robin Lambertz - (@roblabla) for contributing the bulk of the implementation in #593. -* As a result of native notarization, integration with Apple's *Transporter* - has been removed. The ``find-transporter`` command has been removed. Rust - APIs related to Transporter, the *app metadata* XML format it used, and App - Store Connect APIs previously used have been removed. -* As a result of native notarization, UI and implementation details of - notarization have changed. The output when uploading assets is much more - concise. Before, code existed to normalize uploaded assets to a data format - required by Transporter. As a side-effect, assets were somewhat validated - locally before upload. In the new world, minimal checks are performed locally. - This can result in errors (such as attempting to upload an asset without a - code signature) occurring later than they did previously. -* A new ``encode-app-store-connect-api-key`` command can be used to encode an - App Store Connect API Key in a single JSON object. These keys are used for - notarization and having all the API Key metadata in a single file / JSON - blob means you have 1 entity to define your App Store Connect API Key instead - of 3, making UI simpler. -* The ``notarize`` command has been renamed to ``notary-submit``. This follows - the terminology of Apple's ``notarytool`` and mimics the nomenclature used - by the Notary API. The old ``notarize`` command is an alias to - ``notary-submit``. -* The ``notary-submit`` command now has an ``--api-key-path`` argument defining the - path to a JSON file containing the unified App Store Connect API Key emitted - by the ``encode-app-store-connect-api-key`` command. We recommend using this - method for specifying the API Key going forward, as it is simpler. The old - method was required for use with Apple's Transporter application, which we - no longer use so we're no longer bound by its requirements. The old method - will likely be dropped from a future release. -* A new ``notary-wait`` command can be used to wait on a previous notary - submission to complete and to view its log info. This command can be useful if - ``notary-submit`` times out or otherwise fails and you want to query the - status of a previous notarization. -* A new ``notary-log`` command will fetch the notarization log of a previous - submission from the Notary API server. -* Fixed signing of Mach-O binaries having a gap between segments. (This is known - to commonly occur in Go binaries.) In previous versions, we would compute - digests of the file incorrectly and would encounter an assertion when copying - Mach-O data to the output binary. Both of these issues should now be fixed. - (#588 and #616) -* minicbor crate upgraded from version 0.15. This created API differences in - remote signing code. -* The APIs around Mach-O file parsing have been significantly overhauled. It - is probably best to diff the ``macho`` module to see the full differences. - There are now ``MachFile`` and ``MachOBinary`` types serving as interfaces - to custom Mach-O functionality. Most code interfacing with a Mach-O file now - uses these types. The ``AppleSignable`` trait has been deleted as it is no - longer needed since we have the dedicated ``MachOBinary`` type. - -0.16.0 -====== - -(Released 2022-06-05) - -* Distributed macOS binaries no longer dynamically link ``liblzma.5.dylib``. - -0.15.0 -====== - -(Released 2022-06-04) - -* XAR files are now always signed through a temporary file in order to avoid - corruption of the XAR file. - -0.14.0 -====== - -(Released 2022-04-24) - -* Fixed a bug where symlinks weren't been written in notarization zip file - files properly. This prevented bundles containing symlinks from notarizing - correctly. -* The filename used in notarization uploads is now normalized to avoid - rejection due to spaces and colons. -* Support for remote signing. The feature is documented extensively in the - Sphinx documentation. Essentially, 2 independent machines communicate with - each other with end-to-end encrypted messages via a websocket bridged through - a central server. Signing requests are sent to a remote machine which is in - possession of the signing key. Signatures are made on the remote machine and - transmitted back to the originating machine. Remote signing enables signing - to be performed more securely by facilitating signing without having to give - the initiating machine access to the signing key. -* Default log output format has changed. Lines are no longer prefixed with the - time, log level, or logging module by default. A ``-v/--verbose`` global flag - has been added to increase the verbosity of logging. This can restore the - printing of the prefixes. This crate uses - `env_logger `_, so it is possible - to customize default behavior via environment variables. -* The possible values for the ``--code-signature-flags`` are now advertised in - help output. -* Written Mach-O files should now always have their filesystem permissions - preserved. Before, we may not have preserved file permissions in all code - paths writing Mach-O files. -* A new ``keychain-print-certificates`` command can be used to print - certificates available in macOS keychains. -* Initial support for using macOS keychain certificates for code signing. - Previously, we required that certificates be exported from keychain in - order to sign. We now support signing using SecurityFramework APIs so - keys don't have to leave the keychain. Due to a limitation in the Rust - bindings to SecurityFramework, decryption using keychain keys is not - supported. So the *public key agreement* method of remote code signing - will not yet work with keychain-based keys. The new ``--keychain-domain`` - and ``--keychain-fingerprint`` arguments can be used to specify how to - search for and use keychain hosted keys. - -0.13.0 -====== - -(Released 2022-04-10) - -* Restores behavior of <= 0.10.0 where the binary identifier of non main - executable Mach-O files in bundles is automatically derived from the file name - if the Mach-O doesn't already have a binary identifier. This fixes a regression - in 0.11 and 0.12. -* When signing a Mach-O, ``Info.plist`` data embedded in the Mach-O is now - automatically used when no ``Info.plist`` data is provided externally. -* The handling of preserving metadata from previous Mach-O signatures has been - refactored. In the new world, existing Mach-O state is imported into the - signing settings data structure at signing time and the signing operation - largely uses the settings data structure as the canonical source for state. - Explicitly set signing settings should take precedence over a previous Mach-O - signature. -* Fixed a bug where empty Mach-O segments could result in an error when writing - signed Mach-O files. (#544) -* Mach-O and bundle signing now automatically use OS targeting metadata embedded - in Mach-O binaries to activate SHA-1 + SHA-256 digests when necessary. If a - Mach-O binary indicates it targets an older OS version that lacks support for - SHA-256 digests (e.g. macOS <10.11.4), we will automatically use SHA-1 as the - primary digest method and include SHA-256 digests for modern operating systems. - As a result of this change, binaries and bundles that were targeting macOS - <10.11.4, iOS/tvOS <11, and watchOS now properly contain SHA-1 digests as the - primary digest type. -* In bundle signing, ``CodeResources`` files now capture the ``cdhash`` of the - SHA-256 code directory. Before, they would always use the primary code - directory, which might be using SHA-1. The ``cdhash`` value must be from the - SHA-256 code directory to be valid. This change should result in more bundles - having working signatures. -* DER encoded entitlements are now only added when signing executable files. - Previously, we added DER encoded entitlements whenever entitlements data - was present. It appears DER encoded entitlements are only written on Mach-O - binaries that are executables. -* Executable segment flags are now derived from the Mach-O file type and - entitlements plist data. We no longer blindly copy executable segment flags - from previous signatures. We no longer have CLI arguments to define executable - segment flags. This ensures that the entitlements plist and executable - segment flags are always in sync. -* CMS signatures are now properly constructed when there are multiple code - directories. Before, the CMS signed attributes didn't capture all code - directories and the signatures would be incomplete. This resulted in Apple's - tooling rejecting the CMS signatures as invalid. - -0.12.0 -====== - -* Binary identifier strings are now always enclosed in double quotes when - serializing code requirements expressions to strings. Previously, the lack of - double quotes could result in malformed strings that might fail to parse. -* Fixed a bundle signing bug where the digests of nested bundles were taken from the - source directory and not the destination directory. This would result in digests - of nested bundles being incorrect if signing bundles to a different output directory - than from the input. - -0.11.0 -====== - -* The ``--pfx-file``, ``--pfx-password``, and ``--pfx-password-file`` arguments - have been renamed to ``--p12-file``, ``--p12-password``, and - ``--p12-password-file``, respectively. The old names are aliases and should - continue to work. -* Initial support for using smartcards for signing. Smartcard integration may only - work with YubiKeys due to how the integration is implemented. -* A new ``rcodesign smartcard-scan`` command can be used to scan attached - smartcards and certificates they have available for code signing. -* ``rcodesign sign`` now accepts a ``--smartcard-slot`` argument to specify the - slot number of a certificate to use when code signing. -* A new ``rcodesign smartcard-import`` command can be used to import a code signing - certificate into a smartcard. It can import private-public key pair or just import - a public certificate (and use an existing private key on the smartcard device). -* A new ``rcodesign generate-certificate-signing-request`` command can be used - to generate a Certificate Signing Request (CSR) which can be uploaded to Apple - and exchanged for a code signing certificate signed by Apple. -* A new ``rcodesign smartcard-generate-key`` command for generating a new private - key on a smartcard. -* Fixed bug where ``--code-signature-flags``, `--executable-segment-flags``, - ``--runtime-version``, and ``--info-plist-path`` could only be specified once. -* ``rcodesign sign`` now accepts an ``--extra-digest`` argument to provide an - extra digest type to include in signatures. This facilitates signing with - multiple digest types via e.g. ``--digest sha1 --extra-digest sha256``. -* Fixed an embarrassing number of bugs in bundle signing. Bundle signing was - broken in several ways before: resource files in shallow app bundles (e.g. iOS - app bundles) weren't handled correctly; symlinks weren't preserved correctly; - framework signing was completely busted; nested bundles weren't signed in the - correct order; entitlements in Mach-O binaries weren't preserved during - signing; ``CodeResources`` files had extra entries in ```` that shouldn't - have been there, and likely a few more. -* Add ``--exclude`` argument to ``rcodesign sign`` to allow excluding nested - bundles from signing. -* Notarizing bundles containing symlinks no longer fails with a cryptic I/O - error message. We now produce zip files with symlink entries. However, there - may still be issues getting Apple to notarize bundles with symlinks. -* Fixed a bug where we could silently write a softly corrupt code signature - by copying digests that were too short. Previously, if you attempted to re-sign - a Mach-O having SHA-1 digests, those SHA-1 digests could get copied to the - new signature using SHA-256 digests and the bytes belonging to each digest - would get mangled and wouldn't be correct. We now prevent writing digests - that don't match the expected digest length and when copying digests we - look for alternate code directories having the digest of the new signature. - -0.10.0 -====== - -* Support for signing, notarizing, and stapling ``.dmg`` files. -* Support for signing, notarizing, and stapling flat packages (``.pkg`` installers). -* Various symbols related to common code signature data structures have been moved from the - ``macho`` module to the new ``embedded_signature`` module. -* Signing settings types have been moved from the ``signing`` module to the new - ``signing_settings`` module. -* ``rcodesign sign`` no longer requires an output path and will now sign an entity - in place if only a single positional argument is given. -* The new ``rcodesign print-signature-info`` command prints out easy-to-read YAML - describing code signatures detected in a given path. Just point it at a file with - code signatures and it can print out details about the code signatures within. -* The new ``rcodesign diff-signatures`` command prints a diff of the signature content - of 2 filesystem paths. It is essentially a built-in diffing mechanism for the output - of ``rcodesign print-signature-info``. The intended use of the command is to aid - in debugging differences between this tool and Apple's canonical tools. - -0.9.0 -===== - -* Imported new Apple certificates. ``Developer ID - G2 (Expiring 09/17/2031 00:00:00 UTC)``, - ``Worldwide Developer Relations - G4 (Expiring 12/10/2030 00:00:00 UTC)``, - ``Worldwide Developer Relations - G5 (Expiring 12/10/2030 00:00:00 UTC)``, - and ``Worldwide Developer Relations - G6 (Expiring 03/19/2036 00:00:00 UTC)``. -* Changed names of enum variants on ``apple_codesign::apple_certificates::KnownCertificate`` - to reflect latest naming from https://www.apple.com/certificateauthority/. -* Refreshed content of Apple certificates ``AppleAAICA.cer``, ``AppleISTCA8G1.cer``, and - ``AppleTimestampCA.cer``. -* Renamed ``apple_codesign::macho::CodeSigningSlot::SecuritySettings`` to - ``EntitlementsDer``. -* Add ``apple_codesign::macho::CodeSigningSlot::RepSpecific``. -* ``rcodesign extract`` has learned a ``macho-target`` output to display information - about targeting settings of a Mach-O binary. -* The code signature data structure version is now automatically modernized when - signing a Mach-O binary targeting iOS >= 15 or macOS >= 12. This fixes an issue - where signatures of iOS 15+ binaries didn't meet Apple's requirements for this - platform. -* Logging switched to ``log`` crate. This changes program output slightly and removed - an ``&slog::Logger`` argument from various functions. -* ``SigningSettings`` now internally stores entitlements as a parsed plist. Its - ``set_entitlements_xml()`` now returns ``Result<()>`` in order to reflect errors - parsing plist XML. Its ``entitlements_xml()`` now returns ``Result>`` - instead of ``Option<&str>`` because XML serialization is fallible and the resulting - XML is owned instead of a reference to a stored value. As a result of this change, - the embedded entitlements XML specified via ``rcodesign sign --entitlement-xml-path`` - may be encoded differently than it was previously. Before, the content of the - specified file was embedded verbatim. After, the file is parsed as plist XML and - re-serialized to XML. This can result in encoding differences of the XML. This - should hopefully not matter, as valid XML should be valid XML. -* Support for DER encoded entitlements in code signatures. Apple code signatures - encode entitlements both in plist XML form and DER. Previously, we only supported - the former. Now, if entitlements are being written, they are written in both XML - and DER. This should match the default behavior of `codesign` as of macOS 12. - (#513, #515) -* When signing, the entitlements plist associated with the signing operation - is now parsed and keys like ``get-task-allow`` and - ``com.apple.private.skip-library-validation`` are now automatically propagated - to the code directory's executable segment flags. Previously, no such propagation - occurred and special entitlements would not be fully reflected in the code - signature. The new behavior matches that of ``codesign``. -* Fixed a bug in ``rcodesign verify`` where code directory verification was - complaining about ``slot digest contains digest for slot not in signature`` - for the ``Info (1)`` and ``Resources (3)`` slots. The condition it was - complaining about was actually valid. (#512) -* Better supported for setting the hardened runtime version. Previously, we - only set the hardened runtime version in a code signature if it was present - in the prior code signature. When signing unsigned binaries, this could - result in the hardened runtime version not being set, which would cause - Apple tools to complain about the hardened runtime not being enabled. Now, - if the ``runtime`` code signature flag is set on the signing operation and - no runtime version is present, we derive the runtime version from the version - of the Apple SDK used to build the binary. This matches the behavior of - ``codesign``. There is also a new ``--runtime-version`` argument to - ``rcodesign sign`` that can be used to override the runtime version. -* When signing, code requirements are now printed in their human friendly - code requirements language rather than using Rust's default serialization. -* ``rcodesign sign`` will now automatically set the team ID when the signing - certificate contains one. -* Added the ``rcodesign find-transporter`` command for finding the path to - Apple's *Transporter* program (which is used for notarization). -* Initial support for stapling. The ``rcodesign staple`` command can be used - to staple a notarization ticket to an entity. It currently only supports - stapling app bundles (``.app`` directories). The command will automatically - contact Apple's servers to obtain a notarization ticket and then staple - any found ticket to the requested entity. -* Initial support for notarizing. The ``rcodesign notarize`` command can - be used to upload an entity to Apple. The command can optionally wait on - notarization to finish and staple the notarization ticket if notarization - is successful. The command currently only supports macOS app bundles - (``.app`` directories). - -0.8.0 -===== - -* Crate renamed from ``tugger-apple-codesign`` to ``apple-codesign``. -* Fixed bug where signing failed to update the ``vmsize`` field of the - ``__LINKEDIT`` mach-o segment. Previously, a malformed mach-o file could - be produced. (#514) -* Added ``x509-oids`` command for printing Apple OIDs related to code signing. -* Added ``analyze-certificate`` command for printing information about - certificates that is relevant to code signing. -* Added the ``tutorial`` crate with some end-user documentation. -* Crate dependencies updated to newer versions. - -0.7.0 and Earlier -================= - -* Crate was published as `tugger-apple-codesign`. No history kept in this file. diff --git a/apple-codesign/Cargo.toml b/apple-codesign/Cargo.toml deleted file mode 100644 index d0f0964b0..000000000 --- a/apple-codesign/Cargo.toml +++ /dev/null @@ -1,108 +0,0 @@ -[package] -name = "apple-codesign" -version = "0.19.0-pre" -authors = ["Gregory Szorc "] -edition = "2021" -license = "MPL-2.0" -description = "Pure Rust interface to code signing on Apple platforms" -keywords = ["apple", "macos", "codesign"] -homepage = "https://github.com/indygreg/PyOxidizer" -repository = "https://github.com/indygreg/PyOxidizer.git" -readme = "README.md" - -[[bin]] -name = "rcodesign" -path = "src/main.rs" - -[dependencies] -anyhow = "1.0" -aws-config = "0.47" -aws-sdk-s3 = "0.17" -aws-smithy-http = "0.47" -base64 = "0.13" -bcder = "0.7" -bitflags = "1.2" -bytes = "1.0" -clap = "3.1" -chrono = "0.4" -cryptographic-message-syntax = "0.18" -der = "0.6" -dialoguer = "0.10" -difference = "2.0" -digest = "0.10" -dirs = "4.0" -elliptic-curve = { version = "0.12", features = ["arithmetic", "pkcs8"] } -env_logger = "0.9" -filetime = "0.2" -glob = "0.3" -goblin = "0.5" -hex = "0.4" -jsonwebtoken = "8" -log = "0.4" -md-5 = "0.10" -minicbor = { version = "0.18", features = ["derive", "std"] } -oid-registry = "0.6" -once_cell = "1.7" -p12 = "0.6" -p256 = { version = "0.11", default-features = false, features = ["arithmetic", "pkcs8", "std"] } -pem = "1.0" -pkcs1 = { version = "0.4", features = ["alloc", "std"] } -pkcs8 = { version = "0.9", features = ["alloc", "std"] } -plist = "1.2" -rand = "0.8" -rasn = "0.6" -rayon = "1.5" -regex = "1.5" -reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] } -ring = "0.16" -rsa = "0.6" -scroll = "0.11" -sha2 = "0.10" -semver = "1.0" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde_yaml = "0.9" -simple-file-manifest = "0.11" -signature = "1.3" -spake2 = "0.3" -spki = { version = "0.6", features = ["pem"] } -subtle = "2.4" -tempfile = "3.3" -thiserror = "1.0" -tokio = { version = "1.19", features = ["rt"] } -tungstenite = { version = "0.17", features = ["rustls-tls-native-roots"] } -uuid = { version = "1.1", features = ["v4"] } -x509-certificate = "0.15" -x509 = "0.2" -xml-rs = "0.8" -yasna = "0.5" -yubikey = { version = "0.6", optional = true, features = ["untested"] } -zeroize = { version = "1.3", features = ["zeroize_derive"] } -zip = { version = "0.6", default-features = false, features = ["deflate"] } -zip_structs = "0.2" - -[dependencies.apple-bundles] -path = "../apple-bundles" -version = "0.14.0-pre" - -[dependencies.apple-flat-package] -path = "../apple-flat-package" -version = "0.10.0-pre" - -[dependencies.apple-xar] -path = "../apple-xar" -version = "0.10.0-pre" - -[dependencies.tugger-apple] -path = "../tugger-apple" -version = "0.8.0-pre" - -[target.'cfg(target_os = "macos")'.dependencies] -security-framework = { version = "2.6", features = ["OSX_10_12"] } - -[dev-dependencies] -indoc = "1.0" - -[features] -default = [] -smartcard = ["yubikey"] diff --git a/apple-codesign/README.md b/apple-codesign/README.md deleted file mode 100644 index 27e45f7ce..000000000 --- a/apple-codesign/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# apple-codesign - -`apple-codesign` is a crate implementing functionality related to code signing -on Apple platforms. - -All functionality is implemented in pure Rust and doesn't require any 3rd party -or proprietary software nor do we require running on Apple platforms. - -We believe this crate provides the most comprehensive implementation of Apple -code signing outside the canonical Apple tools. We have support for the following -features: - -* Signing Mach-O binaries (the executable file format on Apple operating systems). -* Signing, notarizing, and stapling directory bundles (e.g. `.app` directories). -* Signing, notarizing, and stapling XAR archives / `.pkg` installers. -* Signing, notarizing, and stapling DMG disk images. - -What this all means is that you can sign, notarize, and release Apple software -from anywhere you can get the Rust crate to compile. Linux, Windows, and macOS -are officially supported by other operating systems (like BSDs) should work as -well. - -See the crate documentation at https://docs.rs/apple-codesign/latest/apple_codesign/ -and the end-user documentation at -https://gregoryszorc.com/docs/apple-codesign/main/ for more. - -# `rcodesign` CLI - -This crate defines an `rcodesign` binary which provides a CLI interface to -some of the crate's capabilities. To install: - -```bash -# From a Git checkout -$ cargo run --bin rcodesign -- --help -$ cargo install --bin rcodesign - -# Remote install. -$ cargo install --git https://github.com/indygreg/PyOxidizer --branch main --bin rcodesign apple-codesign -``` - -# Project Relationship - -`apple-codesign` is part of the -[PyOxidizer](https://github.com/indygreg/PyOxidizer.git) project and -this crate is developed in that repository. - -While this crate is developed as part of a larger project, modifications -to support its use outside of its primary use case are very much welcome! diff --git a/apple-codesign/docs/Makefile b/apple-codesign/docs/Makefile deleted file mode 100644 index fd652946f..000000000 --- a/apple-codesign/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -W -n -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/apple-codesign/docs/apple_codesign.rst b/apple-codesign/docs/apple_codesign.rst deleted file mode 100644 index c5d952b35..000000000 --- a/apple-codesign/docs/apple_codesign.rst +++ /dev/null @@ -1,67 +0,0 @@ -.. _apple_codesign: - -================== -Apple Code Signing -================== - -The ``apple-codesign`` Rust crate and its corresponding ``rcodesign`` CLI -tool implement code signing for Apple platforms. - -We believe this crate provides the most comprehensive implementation of Apple -code signing outside the canonical Apple tools. We have support for the following -features: - -* Signing Mach-O binaries (the executable file format on Apple operating systems). -* Signing, notarizing, and stapling directory bundles (e.g. ``.app`` directories). -* Signing, notarizing, and stapling XAR archives / ``.pkg`` installers. -* Signing, notarizing, and stapling disk images / ``.dmg`` files. - -**What this all means is that you can sign, notarize, and release Apple software -from non-Apple operating systems (like Linux, Windows, and BSDs) without needing -access to proprietary Apple software!** - -Other features include: - -* Built-in support for using :ref:`smart cards ` (e.g. - YubiKeys) for signing and key/certificate management. -* A *remote signing* mode that enables you to delegate just the low-level - cryptographic signature generation to a remote machine. This allows you to - do things like have a CI job initiate signing but use a YubiKey on a remote - machine to create cryptographic signatures. See - :ref:`apple_codesign_remote_signing` for more. -* Certificate Signing Request (CSR) support to enable arbitrary private keys - (including those generated on smart card devices) to be easily exchanged for - Apple-issued code signing certificates. -* Support for dumping and diffing data structures related to Apple code - signatures. -* Awareness of Apple's public PKI infrastructure, including CA certificates - and custom X.509 extensions and OIDs used by Apple. -* Documentation and code that are likely a treasure trove for others wanting - to learn and experiment with Apple code signing. - -Canonical project links: - -* Source code: https://github.com/indygreg/PyOxidizer/tree/main/apple-codesign -* Documentation https://gregoryszorc.com/docs/apple-codesign/ -* Rust crate: https://crates.io/crates/apple-codesign -* Changelog: https://github.com/indygreg/PyOxidizer/blob/main/apple-codesign/CHANGELOG.rst -* Bugs and feature requests: https://github.com/indygreg/PyOxidizer/issues?q=is%3Aopen+is%3Aissue+label%3Aapple-codesign - -While this project is developed inside a larger monorepository, it is designed -to be used as a standalone project. - -.. toctree:: - :maxdepth: 2 - - apple_codesign_getting_started - apple_codesign_rcodesign - apple_codesign_certificate_management - apple_codesign_smartcard - apple_codesign_concepts - apple_codesign_quirks - apple_codesign_debugging - apple_codesign_remote_signing - apple_codesign_remote_signing_protocol - apple_codesign_remote_signing_design - apple_codesign_gatekeeper - apple_codesign_custom_assessment_policies diff --git a/apple-codesign/docs/apple_codesign_actions_initiator_output.png b/apple-codesign/docs/apple_codesign_actions_initiator_output.png deleted file mode 100755 index 692fd4ecf..000000000 Binary files a/apple-codesign/docs/apple_codesign_actions_initiator_output.png and /dev/null differ diff --git a/apple-codesign/docs/apple_codesign_actions_signer_output.png b/apple-codesign/docs/apple_codesign_actions_signer_output.png deleted file mode 100755 index eb4ba58c0..000000000 Binary files a/apple-codesign/docs/apple_codesign_actions_signer_output.png and /dev/null differ diff --git a/apple-codesign/docs/apple_codesign_actions_sjs_join.png b/apple-codesign/docs/apple_codesign_actions_sjs_join.png deleted file mode 100755 index 83ac1be7f..000000000 Binary files a/apple-codesign/docs/apple_codesign_actions_sjs_join.png and /dev/null differ diff --git a/apple-codesign/docs/apple_codesign_certificate_management.rst b/apple-codesign/docs/apple_codesign_certificate_management.rst deleted file mode 100644 index d126b3e45..000000000 --- a/apple-codesign/docs/apple_codesign_certificate_management.rst +++ /dev/null @@ -1,235 +0,0 @@ -.. _apple_codesign_certificate_management: - -================================== -Managing Code Signing Certificates -================================== - -In order to add cryptographic signatures using this tool, you'll need to use -a :ref:`apple_codesign_code_signing_certificate`. (Follow the link for what -that means.) - -In order to perform code signing in a way that is recognized and trusted by Apple -operating systems, you will need to obtain a code signing certificate that is -signed/issued by Apple. This requires joining the -`Apple Developer Program `_, which has an -annual membership fee. - -Once you are a member, there are various ways to generate and manage your -certificates. But first, a primer about flavors of Apple code signing -certificates. - -Apple Code Signing Certificate Flavors -====================================== - -Apple issues different types/flavors of code signing certificates. Each one is -used to sign a different class of software. - -If you are logged into your Apple Developer account, you can see Apple's -description for these at https://developer.apple.com/account/resources/certificates/add. -Here's our concise definitions: - -*Apple Development* - Sign applications for Apple operating systems that aren't distributed publicly. - -*Apple Distribution* - Sign applications for submission to the App Store or for Ad Hoc distribution. - -*iOS App Development* - Legacy version of *Apple Development* just for iOS apps. (We think.) - -*iOS Distribution* - Legacy version of *Apple Distribution* just for iOS apps. (We think.) - -*Mac Development* - Legacy version of *Apple Development* just for macOS apps. (We think.) - -*Mac App Distribution* - Sign macOS applications and configure a Distribution Provisioning Profile - for distribution through Mac App Store. - -*Mac Installer Distribution* - Sign package installers (e.g. ``.pkg`` files) which will be distributed via the - Mac App Store. - -*Developer ID Installer* - Sign package installers (e.g. ``.pkg`` files) which will be distributed outside - the Mac App Store. i.e. if users fetch your installer via your website, you sign - with this. - -*Developer ID Application* - Sign applications which will be distributed outside the Mac App Store. Used for - signing Mach-O binaries, ``.app`` bundles, and ``.dmg`` files. - -Essentially, if you are distributing macOS software to end-users via non-Apple -channels like your website, you need *Developer ID Application* and/or *Developer ID -Installer*. - -If you are distributing via Apple's App stores, you need *Apple Distribution* or one -of the other types having *Distribution* in the name. - -.. tip:: - - The ``rcodesign analyze-certificate`` command can be used to print information - about Apple code signing certificates. Look for a line with ``Certificate Profile`` - in its output to see which flavor of certificate this software thinks it is. - -Generating Certificates with Xcode -================================== - -Using Xcode from macOS is probably the easiest way to create and manage -your certificates as Xcode has built-in UI to facilitate this. - -Apple keeps thorough -`documentation about how to do this `_. -Please follow Apple's documentation to generate a certificate. - -Obtaining a Certificate via a Certificate Signing Request -========================================================= - -You can obtain a code signing certificate by uploading a *Certificate Signing -Request (CSR)* to Apple. Essentially, you generate a CSR, send it to Apple, -and Apple will issue a new code signing certificate which you can download. - -A CSR is produced by creating a cryptographic signature (using a *private -key*) over a small set of metadata describing the *private key* for which -a certificate shall be issued. - -In order to generate a CSR, you need a *private key*. As of April 2022, Apple -appears to require the use of RSA 2048 private keys. - -If you have access to macOS, the easiest way to generate a private key and -CSR is to use ``Keychain Access`` using the -`procedure outlined here `_. - -If you want to generate your own CSR using ``rcodesign``, you can! First, -you'll need a private key. - -To generate an RSA 2048 private key using OpenSSL:: - - openssl genrsa -out private.pem 2048 - -.. warning:: - - The RSA private key will be in plain text on your filesystem. This is not - very secure! - -Then once you have a private key, we can generate a CSR using ``rcodesign``:: - - rcodesign generate-certificate-signing-request --pem-source private.pem - rcodesign generate-certificate-signing-request --p12-file key.p12 - - # Smart cards require generating a new key then creating a CSR from that key. - rcodesign smartcard-generate-key --smartcard-slot 9c - rcodesign generate-certificate-signing-request --smartcard-slot 9c - -This command will print the CSR to stdout. e.g.:: - - -----BEGIN CERTIFICATE REQUEST----- - MIHeMIGDAgEAMCExHzAdBgNVBAMMFkFwcGxlIENvZGUgU2lnbmluZyBDU1IwWTAT - BgcqhkjOPQIBBggqhkjOPQMBBwNCAAQxluBlPIv/HgBDz0O3GLPhhna/NJU7menq - GzUc9sZFOgZ7XmpR9vQTxHPEyg5D6huBapVQZsDG9IgAXjvSOmimoAAwDAYIKoZI - zj0EAwIFAANIADBFAiEAoZpbfrlm7HgQXByfwuoPt7/V+QM7DCIILcTKCBrkIZUC - IEIp8yA9bSg7bM9XJl8bgFesTjermlSYQI/2JY834/z7 - -----END CERTIFICATE REQUEST----- - -You probably want to use ``--csr-pem-path`` to write that to a file automatically:: - - rcodesign generate-certificate-signing-request --smartcard-slot 9c --csr-pem-path csr.pem - -.. _apple_codesign_exchange_csr: - -Exchanging a CSR for a Code Signing Certificate ------------------------------------------------ - -Once you have a CSR file, you can attempt to exchange it for a code signing -certificate. - -1. Go to https://developer.apple.com/account/resources/certificates/add (you must be - logged into Apple's website) -2. Select the certificate *flavor* you want to issue. -3. Click ``Continue`` to advance to the next form. -4. Select the ``G2 Sub-CA (Xcode 11.4.1 or later)`` *Profile Type* (we support it). -5. Choose the file containing your CSR. -6. Click ``Continue``. -7. If all goes according to plan, you should see a page saying ``Download Your - Certificate``. -8. Click the ``Download`` button. -9. Save the certificate somewhere. (The file content is likely not sensitive and - doesn't need to be kept secret because this content will be copied to everything - you sign with it!) - -At this point, you have both a *private key* and a *public certificate*: you can -sign Apple software! - -Exporting a Code Signing Certificate to a File -============================================== - -``rcodesign`` supports consuming code signing certificates from multiple -sources, including hardware devices. But sometimes it is desirable to have -your code signing certificate exist as a file. - -Use the instructions in one of the following sections to export a code signing -certificate. - -.. danger:: - - It is generally accepted that private keys stored in files are less - secure than stored in special operating system enclaves like keychains. - This is because the operating system has protections around accessing - the private keys and these protections are often much stronger than - those on a file on the filesystem. - - This tool has support for using certificates / keys directly from - macOS keychains. So exporting to a file is not always necessary. - -Using Keychain Access ---------------------- - -(macOS) - -1. Open the ``Keychain Access`` application. -2. Find the certificate you want to export and command click or right click on it. -3. Select the ``Export`` option. -4. Choose the ``Personal Information Exchange (.p12)`` format and select a - file destination. -5. Enter a password used to protect the contents of the certificate. -6. If prompted to enter your system password to unlock your keychain, do so. - -The exported certificate is in the PKCS#12 / PFX / p12 file format. Command -arguments with these labels in the same can be used to interact with the -exported certificate. - -Using Xcode ------------ - -(macOS) - -See `Apple's Xcode documentation `_. - -Using ``security`` ------------------- - -(macOS) - -1. Run ``security find-identity`` to locate certificates available for export. -2. Run ``security export -t identities -f pkcs12 -o keys.p12`` - -If you have multiple identifies (which is common), ``security export`` will export -all of them. ``security`` doesn't seem to have a command to export just a single -certificate pair. You will need to invoke some ``openssl`` command to extract -just the certificate you care about. Please contribute back a fix for this -documentation once you figure it out! - -Using a Self-Signed Certificate -=============================== - -If you want to cut some corners and play around with certificates not -signed by Apple, you can run ``rcodesign generate-self-signed-certificate`` -to generate a self-signed code signing certificate. - -This command will include special attributes in the certificate that indicate -compatibility with Apple code signing. However, since the certificate isn't -signed by Apple, its signatures won't confer the same trust that Apple signed -certificates would. - -These certificates can be useful for debugging and testing. diff --git a/apple-codesign/docs/apple_codesign_concepts.rst b/apple-codesign/docs/apple_codesign_concepts.rst deleted file mode 100644 index 635a45872..000000000 --- a/apple-codesign/docs/apple_codesign_concepts.rst +++ /dev/null @@ -1,158 +0,0 @@ -.. _apple_codesign_concepts: - -======== -Concepts -======== - -Code signing on Apple platforms is complex and has many parts. This -document aims to shed some light on things. - -Cryptographic Signatures -======================== - -At the heart of code signing is the use of cryptographic signatures. - -The Wikipedia article on -`digital signatures `_ explains -the concept in far more detail than we care to go into. - -Essentially, mathematics is used to prove that an entity in possession of a -secret *key* digitally attested to the existence of some *signed* entity. - -More concretely, an X.509 code signing certificate can be proved to have -signed some piece of software by inspecting the cryptographic signature it -produced. - -Apple's cryptographic signatures use RFC 5652 / Cryptographic Message Syntax -(CMS) for representing signatures. This standardized format is used outside -the Apple ecosystem and libraries and tools like OpenSSL are capable of -interfacing with it. - -Code Signing -============ - -*Code signing* (or just *signing*) is the mechanism of producing (and then -attaching) a signature to some entity. - -Typically signing entails producing a cryptographic signature using a code -signing certificate. However, Mach-O files (the binary file format for -Apple platforms) has a concept of *ad-hoc* signing where the binary has -data structures describing the content of the binary but without the -cryptographic signature present. - -Notarization -============ - -*Notarization* is the term Apple gives to the process of uploading an asset -to Apple for inspection. - -In order to help safeguard and control their software ecosystems, Apple -imposes requirements that applications and installers be inspected by Apple -before they are allowed to run on Apple operating systems - either at all -or without scary warning signs. - -When you notarize software, you are essentially asking for Apple's blessing -to distribute that software. If Apple's systems are appeased, they will -issue a *notarization ticket*. - -Notarization Ticket -=================== - -A *notarization ticket* is a blob of data that essentially proves that Apple -notarized a piece of software. - -The exact format and content of *notarization tickets* is not well known. But -they do contain some DER-encoded ASN.1 with data structures that common appear -in X.509 certificates. All that matters is that Apple's operating systems know -how to read and validate a notarization ticket. - -Stapling -======== - -*Stapling* is the term Apple gives to the process of attaching a *notarization -ticket* to some entity. It is literally just fetching a *notarization ticket* -from Apple's servers and then making that ticket available on the entity that -was notarized. - -You can think of notarization and stapling as Apple-issued cryptographic -signatures. It establishes a chain of trust between some entity to you -that also had to be inspected by Apple first. - -Mach-O Binaries -=============== - -`Mach-O `_ is the binary executable -file format used on Apple operating systems. - -When you run an executable like ``/usr/bin/zsh`` on macOS, you are running -a Mach-O file. - -Mach-O binaries are either *thin* or *fat*. A *thin* Mach-O contains code -for a single architecture, like x86-64 or aarch64 / arm64. A *fat* or -*universal* binary contains code for multiple architectures. At run-time, -the operating system will decide which one to execute. - -Bundles -======= - -`Bundles `_ -are a filesystem based mechanism for encapsulating code and resources. - -On macOS, you commonly encounter bundles as ``.app`` and ``.framework`` -directories in ``/Applications`` and ``/System/Library/Frameworks``. - -Bundles are essentially a well-defined set of files that the operating -system knows how to interact with. For example, macOS knows that to -execute an ``.app`` bundle it should look for a ``Contents/Info.plist`` -to resolve basic application metadata, such as the name of the main -binary for the bundle, which resides in ``Contents/MacOS/`` within the -bundle. - -DMGs / Disk Images -================== - -`Apple Disk Images `_ are a -self-contained file format for holding filesystems. Think of DMGs -as standalone hard drives that Apple operating systems can recognize. - -DMGs are often used to distribute macOS applications. - -XARs / Flat Packages / ``.pkg`` Installers -========================================== - -*Flat packages* is a mechanism for installing software. - -They take the form of ``.pkg`` files, which are actually XAR archives -(a tar-like format for storing content for multiple files within a single -file). - -.. _apple_codesign_code_signing_certificate: - -Code Signing Certificate -======================== - -A code signing certificate is used to produce cryptographic signatures over -some signed entity. - -A code signing certificate consists of a private/secret key (essentially a bunch -of large numbers or parameters) and a public certificate which describes it. - -Code signing certificates are X.509 certificates. X.509 certificates are the -same technology used to secure communication with https:// websites. However, -the certificates are used for signing content instead of encrypting it. - -The X.509 public certificate contains a bunch of metadata describing the -certificate. This includes the name of the person or entity it belongs to, -a date range for when it is valid, and a cryptographic signature attesting -to its origination. - -Apple's operating systems look for special metadata on code signing -certificates to authenticate and trust them. There are special properties -on certificates indicating what Apple software distribution they are allowed -to perform. For example, a ``Developer ID Application`` certificate is required -for signed Mach-O binaries, bundles, and DMG files to be trusted and a -``Developer ID Installer`` certificate is required to sign ``.pkg`` installers -in order for them to be trusted. - -In addition, different Apple code signing certificates are cryptographically -signed by different Apple Certificate Authorities (CAs). diff --git a/apple-codesign/docs/apple_codesign_custom_assessment_policies.rst b/apple-codesign/docs/apple_codesign_custom_assessment_policies.rst deleted file mode 100644 index 5823409b5..000000000 --- a/apple-codesign/docs/apple_codesign_custom_assessment_policies.rst +++ /dev/null @@ -1,160 +0,0 @@ -.. _apple_codesign_custom_assessment_policies: - -================================================================ -Selectively Bypassing Gatekeeper with Custom Assessment Policies -================================================================ - -By default, Apple locks down their operating systems such that the default -assessment policies enforced by Gatekeeper restrict what can be run. The -restrictions vary by operating system (iOS is more locked down than macOS for -example). - -On macOS, it is possible to change the system assessment policies via -the ``spctl`` tool. By injecting your own rules, you can allow binaries -through meeting criteria expressible via *code requirements language -expressions*. This allows you to allow binaries having: - -* A specific *code directory hash* (uniquely identifies the binary). -* A specific code signing certificate identified by its certificate hash. -* Any code signing certificate whose trust/signing chain leads to a trusted - certificate. -* Any code signing certificate signed by a certificate containing a - certain X.509 extension OID. -* A code signing certificate with specific values in its subject field. -* And many more possibilities. See - `Apple's docs `_ - on the requirements language for more possibilities. - -Defining custom rules is possible via the under-documented -``spctl --add --requirement`` mode. In this mode, you can register a code -requirements expression into the system database for Gatekeeper to -utilize. The following sections give some examples of this. - -Verifying Assessment Policies -============================= - -The sections below document how to define custom assessment policies -to allow execution of binaries/installers/etc signed by certificates -that aren't normally supported. - -When doing this, you probably want a way to verify things work as -expected. - -The ``spctl --assess`` mode puts ``spctl`` in *assessment mode* and tells you -what verdict Gatekeeper would render. e.g.:: - - $ spctl --assess --type execute -vv /Applications/Firefox.app - /Applications/Firefox.app: accepted - source=Notarized Developer ID - -Do note that this only works on app bundles (not standalone executable -binaries)! If you run ``spctl --assess`` on a standalone executable, you -get an error:: - - $ spctl --assess -vv /usr/bin/ssh - /usr/bin/ssh: rejected (the code is valid but does not seem to be an app) - origin=Software Signing - -In addition, macOS uses the ``com.apple.quarantine`` extended file attribute -to *quarantine* files and prevent them from running via the graphical UI. -It can sometimes be handy to add this attribute back to a file to simulate -a fresh quarantine. You can do this by running a command like the following:: - - xattr -w com.apple.quarantine "0001;$(printf %x $(date +%s));manual;$(/usr/bin/uuidgen)" /path/to/file - -(This extended attribute isn't added to files downloaded by tools like ``curl`` -or ``wget`` which is why you can execute binaries obtained via these tools but -can't run the same binary downloaded via a web browser.) - -Allowing Execution of Binaries Signed by a Specific Certificate -=============================================================== - -Say you have a single code signing certificate and want to be able to -run all binaries signed by that certificate. We can construct a -*code requirement expression* that refers to this specific certificate. - -The most reliable way to specify a single certificate is via a -digest of its content. Assuming no two certificates have the same -digest, this uniquely identifies a certificate. - -You can use ``rcodesign analyze-certificate`` to locate a certificate's -content digest.:: - - rcodesign analyze-certificate --pem-source path/to/cert | grep fingerprint - SHA-1 fingerprint: 0b724bcd713c9f3691b0a8b0926ae0ecf9e7edd8 - SHA-256 fingerprint: ac5c4b5936677942e017bca1570aaa9e763674c4b66709231b15118e5842aeca - -The *code requirement* language only supports SHA-1 hashes. So we -construct our expression referring to this certificate as -``certificate leaf H"0b724bcd713c9f3691b0a8b0926ae0ecf9e7edd8"``. - -Now, we define an assessment rule to allow execution of binaries -signed with this certificate:: - - sudo spctl --add --type execute --label 'My Cert' --requirement \ - 'certificate leaf H"0b724bcd713c9f3691b0a8b0926ae0ecf9e7edd8"' - -Now Gatekeeper should allow execution of all binaries signed with this -exact code signing certificate! - -If the signing certificate hash is registered in the system assessment -policy database, there is no need to register the certificate in a -*keychain* or mark that certificate as *trusted* in a keychain. The signing -certificate also does not need to chain back to an Apple certificate. -And since the requirement expression doesn't say ``and notarized``, binaries -don't need to be notarized by Apple either. **This effectively allows you -to sidestep the default requirement that binaries be signed and notarized -by certificates that Apple is aware of.** Congratulations, you've just -escaped Apple's walled garden (at your own risk of course). - -Do note that for files with the ``com.apple.quarantine`` extended attribute, -you may see a dialog the first time you run this file. You can prevent that -by removing the extended attribute via -``xattr -d com.apple.quarantine /path/to/file``. - -Allowing Execution of Binaries Signed by a Trusted CA -===================================================== - -Say you are an enterprise or distributed organization and want to have -multiple code signing certificates. Using the approach in the section -above you could individually register each code signing certificate you -want to allow. However, the number of certificates can quickly grow and -become unmanageable. - -To solve this problem, you can employ the strategy that Apple itself uses -for code signing certificates associated with Developer ID accounts: trust -code signing certificates themselves issued/signed by a trusted certificate -authority (CA). - -To do this, we'll again craft a *code requirement expression* referring to -our trusted CA certificate. - -This looks very similar to above except we change the position of the -trusted certificate:: - - sudo spctl --add --type execute --label 'My Trusted CA' --requirement \ - 'certificate 1 H"0b724bcd713c9f3691b0a8b0926ae0ecf9e7edd8"' - -That ``certificate 1`` says to apply to the certificate that signed the -certificate that produced the code signature. By trusting the CA certificate, -you implicitly trust all certificates signed by that CA certificate. - -Note that if you use a custom CA for signing code signing certificates, -you'll probably want to follow some best practices for running your own -Public Key Infrastructure (PKI) like publishing a Certificate Revocation List -(CRL). This is a complex topic outside the scope of this documentation. Ask -someone with *Security* in their job title for assistance. - -For CA certificates issuing/signing code signing certificates, you'll -want to enable a few X.509 certificate extensions: - -* Key Usage (``2.5.29.15``): *Digital Signature* and *Key Cert Sign* -* Basic Constraints (``2.5.29.19``): CA=yes -* Extended Key Usage (``2.5.29.37``): Code Signing (``1.3.6.1.5.5.7.3.3``); critical=true - -You can create CA certificates in the ``Keychain Access`` macOS application. -If you create CA certificates another way, you may want to compare certificate -extensions and other fields against those produced via ``Keychain Access`` to -make sure they align. It is unknown how much Apple's operating systems -enforce requirements on the X.509 certificates. But it is a good idea to -keep things as similar as possible. diff --git a/apple-codesign/docs/apple_codesign_debugging.rst b/apple-codesign/docs/apple_codesign_debugging.rst deleted file mode 100644 index 6fec8f81e..000000000 --- a/apple-codesign/docs/apple_codesign_debugging.rst +++ /dev/null @@ -1,49 +0,0 @@ -.. _apple_codesign_debugging: - -================================ -How to Debug and Report Problems -================================ - -Apple code signing is complex and there will be cases where this tool -behaves differently from Apple's, possibly to the point where Apple rejects -the output of this tool. - -.. important:: - - If Apple software rejects the output of this tool, we consider that a bug. - We encourage end-users to report these bugs to the - `GitHub issue tracker `_. - -Commands to Print Signature Info -================================ - -The ``rcodesign print-signature-info`` command can be used to dump YAML -describing any signable file entity. Just point it at a Mach-O, bundle, DMG, -or ``.pkg`` installer and it will tell you what it knows about the entity. - -The ``rcodesign diff-signatures`` command will internally execute -``print-signature-info`` against 2 paths and print the differences between them. - -``rcodesign diff-signatures`` is exceptionally useful at understanding -differences in behavior between this tool and Apple's. If Apple is rejecting -the output of this tool, comparing the output of the same operation with Apple's -tooling against this tool's is a good way to find the source of the problem. - -Reporting Actionable Bugs -========================= - -Please include the following in bug reports to improve chances for action: - -* The released version or Git commit that this tool was built from. -* The command line used. -* The full output of the command. -* The output of ``rcodesign diff-signatures`` comparing similar operations - between Apple's tooling and ours. -* A copy of the entity you were attempting to sign. -* Text copy or screenshot of error from Apple tooling indicating what failed. - -It is understandable that some people may not desire to file publish issue -reports or submit a copy of their application to be seen by the world. If -you send a polite email to gregory.szorc@gmail.com with ``apple-codesign`` or -``rcodesign`` in the subject line along with more private/sensitive details, -support can be given over email. diff --git a/apple-codesign/docs/apple_codesign_gatekeeper.rst b/apple-codesign/docs/apple_codesign_gatekeeper.rst deleted file mode 100644 index ac40f09c3..000000000 --- a/apple-codesign/docs/apple_codesign_gatekeeper.rst +++ /dev/null @@ -1,151 +0,0 @@ -.. _apple_codesign_gatekeeper: - -====================== -A Primer on Gatekeeper -====================== - -*Gatekeeper* is the name Apple gives to a set of technologies that enforce -application execution policies at the operating system level. Essentially, -Gatekeeper answers the question *is this software allowed to run*. - -When Gatekeeper runs, it performs a *security assessment* against the -binary and the currently configured system policies from the system policy -database (see ``man syspolicyd``). If the binary fails to meet the requirements, -Gatekeeper prevents the binary from running. - -The ``spctl`` Tool -================== - -The ``spctl`` program distributed with macOS allows you to query and -manipulate the assessment policies. - -If you run ``sudo spctl --list``, it will print a list of rules. e.g.:: - - $ sudo spctl --list - 8[Apple System] P20 allow lsopen - anchor apple - 3[Apple System] P20 allow execute - anchor apple - 2[Apple Installer] P20 allow install - anchor apple generic and certificate 1[subject.CN] = "Apple Software Update Certification Authority" - 17[Testflight] P10 allow execute - anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.1] exists and certificate leaf[field.1.2.840.113635.100.6.1.25.1] exists - 10[Mac App Store] P10 allow install - anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.10] exists - 5[Mac App Store] P10 allow install - anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.10] exists - 4[Mac App Store] P10 allow execute - anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9] exists - 16[Notarized Developer ID] P5 allow lsopen - 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 notarized - 12[Notarized Developer ID] P5 allow install - 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.14] or certificate leaf[field.1.2.840.113635.100.6.1.13]) and notarized - 11[Notarized Developer ID] P5 allow execute - 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 notarized - 9[Developer ID] P4 allow lsopen - 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 legacy - 7[Developer ID] P4 allow install - 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.14] or certificate leaf[field.1.2.840.113635.100.6.1.13]) and legacy - 6[Developer ID] P4 allow execute - 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[timestamp.1.2.840.113635.100.6.1.33] absent or certificate leaf[timestamp.1.2.840.113635.100.6.1.33] < timestamp "20190408000000Z") - 2718[GKE] P0 allow lsopen [(gke)] - cdhash H"975d9247503b596784dd8a9665fd3ff43eb7722f" - 2717[GKE] P0 allow execute [(gke)] - cdhash H"cf782d6467be86b73a83d86cd6d8c9f87d9d9ce5" - ... - 18[GKE] P0 allow lsopen [(gke)] - cdhash H"cf5f88b3b2ff4d8612aabb915f6d1f712e16b6f2" - 15[Unnotarized Developer ID] P0 deny lsopen - 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 - 14[Unnotarized Developer ID] P0 deny install - 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.14] or certificate leaf[field.1.2.840.113635.100.6.1.13]) - 13[Unnotarized Developer ID] P0 deny execute - 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[timestamp.1.2.840.113635.100.6.1.33] exists and certificate leaf[timestamp.1.2.840.113635.100.6.1.33] >= timestamp "20190408000000Z") - ``` - -The first line of each item identifies the policy. The second line is a -*code requirement language expression*. This is a DSL that compiles to a -binary expression tree for representing a test to perform against a binary. -See ``man csreq`` for more. - -Some of these expressions are pretty straightforward. For example, -the following entry says to allow executing a binary with a code signature -whose *code directory* hash is ``cf782d6467be86b73a83d86cd6d8c9f87d9d9ce5``:: - - 2717[GKE] P0 allow execute [(gke)] - cdhash H"cf782d6467be86b73a83d86cd6d8c9f87d9d9ce5" - -The *code directory* refers to a data structure within the code -signature that contains (among other things) content digests of the binary. The -hash/digest of the code directory itself is effectively a chained digest to the -actual binary content and theoretically a unique way of identifying a binary. So -``cdhash H"cf782d6467be86b73a83d86cd6d8c9f87d9d9ce5"`` is a very convoluted -way of saying *allow this specific binary (specified by its content hash) -to execute*. - -Other rules are more interesting. For example:: - - 11[Notarized Developer ID] P5 allow execute - 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 notarized - -We see the description (``Notarized Developer ID``) but what does that -expression mean? - -Well, first this expression parses into a tree. We won't attempt to format -the tree here. But essentially the following conditions must ``all`` be true: - -* ``anchor apple generic`` -* ``certificate 1[field.1.2.840.113635.100.6.2.6] exists`` -* ``certificate leaf[field.1.2.840.113635.100.6.1.13] exists`` -* ``notarized`` - -``anchor apple generic`` and ``notarized`` are essentially special expressions -that expand to mean *the certificate signing chain leads back to an Apple -root certificate authority (CA)* and *there is a supplemental code signature -from Apple that can only come from Apple's notarization service*. - -But what about those ``certificate`` expressions? That -``certificate [field.*]`` syntax essentially says *the code signature -certificate at ```` in the certificate chain has an X.509 certificate -extension with OID ``X``* (where ``X`` is a value like ``A.B.C.D.E.F``). - -This is all pretty low level. But essentially X.509 certificates can have -a series of *extensions* that further describe the certificate. Apple code -signing uses these extensions to convey metadata about the certificate. And -since code signing certificates are signed, whoever signed those certificates -is effectively also approving of whatever is conveyed by the extensions -within. - -But what do these extensions actually mean? Running ``rcodesign x509-oids`` -may give us some help:: - - $ rcodesign x509-oids` - ... - Code Signing Certificate Extension OIDs - ... - 1.2.840.113635.100.6.1.13 DeveloperIdApplication - ... - Certificate Authority Certificate Extension OIDs - ... - 1.2.840.113635.100.6.2.6 DeveloperId - -We see ``1.2.840.113635.100.6.2.6`` is the OID of an extension on -certificate authorities indicating they act as the *Apple Developer -ID* certificate authority. We also see that ``1.2.840.113635.100.6.1.13`` -is the OID of an extension saying the certificate acts as a code signing -certificate for *applications* associated with an *Apple Developer ID*. - -So, what this expression translates to is essentially: - -* Trust code signatures whose certificate signing chain leads back to an - Apple CA. -* The signer of the code signing certificate must have the extension that - identifies it as the *Apple Developer ID* certificate authority. -* The code signing certificate itself must have the extension that says - it is an *Apple Developer ID* for use with *application* signing. -* The binary is *notarized*. - -In simple terms, this is saying *allow execution of binaries that -were signed by a Developer ID code signing certificate which was signed -by Apple's Developer ID certificate authority and are also notarized*. diff --git a/apple-codesign/docs/apple_codesign_getting_started.rst b/apple-codesign/docs/apple_codesign_getting_started.rst deleted file mode 100644 index c0dbbc0e3..000000000 --- a/apple-codesign/docs/apple_codesign_getting_started.rst +++ /dev/null @@ -1,117 +0,0 @@ -.. _apple_codesign_getting_started: - -=============== -Getting Started -=============== - -Installing -========== - -Pre-built binaries are published as GitHub Releases. Go to -https://github.com/indygreg/PyOxidizer/releases and look for the latest -release of ``Apple Codesign``. - -To install the latest release version of the ``rcodesign`` executable using Cargo -(Rust's package manager): - -.. code-block:: bash - - cargo install apple-codesign - -To enable smart card integration (i.e. use a YubiKey for signing): - -.. code-block:: bash - - cargo install --features smartcard apple-codesign - -To compile and run from a Git checkout of its canonical repository (developer mode): - -.. code-block:: bash - - cargo run --bin rcodesign -- --help - -To install from a Git checkout of its canonical repository: - -.. code-block:: bash - - cargo install --bin rcodesign - -To install from the latest commit in the canonical Git repository: - -.. code-block:: bash - - cargo install --git https://github.com/indygreg/PyOxidizer --branch main rcodesign - -Obtaining a Code Signing Certificate -==================================== - -Follow the instructions at :ref:`apple_codesign_certificate_management` to obtain -a code signing certificate. This is required if signing software for -distribution to other machines. - -If you just want to play around, you can use -``rcodesign generate-self-signed-certificate`` to create a self-signed -certificate. - -.. _apple_codesign_app_store_connect_api_key: - -Obtaining an App Store Connect API Key -====================================== - -To notarize and staple, you'll need an App Store Connect API Key to -authenticate connections to Apple's servers. - -You can generate one at https://appstoreconnect.apple.com/access/api. - -This requires joining the Apple Developer Program, which has an annual -fee. - -See -https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api -for Apple's official documentation on creating these API Keys. - -.. important:: - - For the *Access Role*, ``Developer`` should be sufficient. - - Other roles may or may not work for notarization. - -App Store Connect API Keys have 3 components: - -* An *Issuer ID* (likely a UUID). -* A *Key ID* (an alphanumeric string like ``DEADBEEF42``). -* A PEM encoded ECDSA private key (a file beginning with - ``-----BEGIN PRIVATE KEY-----`` that you can download at most - once when you create an API Key). - -All 3 of these components are required to talk to the App Store Connect -API server. To make management of these keys simpler, we provide the -``encode-app-store-connect-api-key`` command to write out a JSON document -holding all the key info. - -.. important:: - - We highly recommend using our JSON keys created with - ``encode-app-store-connect-api-key`` as it is simpler to manage a single - entity instead of 3. - -You can perform an encode of your key as follows: - -.. code-block:: bash - - rcodesign encode-app-store-connect-api-key -o ~/.appstoreconnect/key.json \ - /path/to/downloaded/private_key - -e.g. - -.. code-block:: bash - - rcodesign encode-app-store-connect-api-key -o ~/.appstoreconnect/key.json \ - 11dda589-8632-49a8-a432-03b5e17fe1d2 DEADBEEF42 ~/Downloads/AuthKey_DEADBEAF42.p8 - -Next Steps -========== - -Once you have a code signing certificate and/or App Store Connect API Key, -read :ref:`apple_codesign_rcodesign` to learn how to sign and/or notarize -software. diff --git a/apple-codesign/docs/apple_codesign_quirks.rst b/apple-codesign/docs/apple_codesign_quirks.rst deleted file mode 100644 index 03eeb8473..000000000 --- a/apple-codesign/docs/apple_codesign_quirks.rst +++ /dev/null @@ -1,145 +0,0 @@ -.. _apple_codesign_quirks: - -============================ -Known Issues and Limitations -============================ - -Apple code signing is complex. While this project strives to provide -all the features and compatibility that Apple's official tooling provides, -we won't always get it right. This document captures some of the areas where -we know we fall short. - -Bundle Handling in General -========================== - -Bundle signing is complex for a few reasons: - -* The types and layouts of bundles are highly varied. Application bundles. - Frameworks. Kernel extensions. macOS flavored vs iOS flavored bundles. The - list goes on. -* Bundles can be nested. -* Signatures in nested bundles often need to propagate to their parent bundle. -* Bundles encapsulate other signable entities, notably Mach-O binaries. - -All this complexity means bundle signing is susceptible to a lot of subtle -bugs and variation from how Apple's tooling does it. - -If you find bugs in bundle signing or have suggestions for improving its -ergonomics, please `file a GitHub issue `_! - -Cannot Sign File Contents of DMGs -================================= - -We support signing DMGs. But we can't recursively inspect the files within -DMGs and sign those. e.g. if a DMG contains a Mach-O binary, we can't -sign that Mach-O by unpacking it from the DMG and writing a new DMG. - -The reason we can't do this is because DMGs contain a nested filesystem -(likely HFS+) and we don't (yet) have a cross-platform mechanism for reading -and writing HFS+ filesystems. - -On macOS, we could call out to ``hdiutil`` to mount a DMG to see its -contents and again to create a new DMG. However, this isn't implemented -because we don't perceive there to be value in it: if you have access to -macOS you should probably just use Apple's official signing tooling! - -There are open source libraries for reading and writing HFS+ filesystems. -We could potentially integrate those to support reading and writing the -contents of DMGs. We could also potentially leverage a pure Rust HFS+ -implementation (this is a preferred solution). - -DMG also supports multiple embedded filesystem types and it is possible -we could leverage one that isn't HFS+ (or APFS) and produce working DMGs. -This is an area we haven't yet explored. - -If you want to distribute DMGs signed with this tool that themselves have -signed files, you'll need to sign the files inside the DMG before the DMG -is created. Then you'll need to create the DMG (using ``hdiutil`` or -whatever tool you have access to) then feed that DMG into this tool for -signing. - -https://github.com/indygreg/PyOxidizer/issues/540 is our tracking issue -for DMG writing support. If you have ideas, please comment there! - -Cannot Recursively Sign Flat Packages (``.pkg`` Installers) -=========================================================== - -Flat Packages (``.pkg`` installers) are a complex file format. - -We have support for signing ``.pkg`` installers by reading the files -within a flat package. And we are capable of recursively extracting -and signing the ``.pkg`` installers that themselves are often embedded -in ``.pkg`` installers. - -What we don't yet have support for is mutating the file content within -flat packages / ``.pkg`` installers. This means we can't recursively sign -nested ``.pkg`` installers or bundles or Mach-O binaries within. - -The main blocker to implementing ``.pkg`` writing is support for -reading and writing Apple's *Bill of Materials* file format. These are -the ``Bom`` files within flat packages. The author of this project -has an unpublished Rust crate to read and write bom files but he -encountered issues getting it to write files that validate with Apple's -implementation. - -So if you want to sign ``.pkg`` files that themselves containable signable -entities, you need to sign files going into the ``.pkg`` before creating -the ``.pkg``. Then you need to create the ``.pkg`` and invoke this tool to -sign the ``.pkg``. For installers that contained nested ``.pkg`` installers, -this process will be quite tedious. Invoking ``componentbuild`` and -``productbuild`` will likely be much simpler. - -https://github.com/indygreg/PyOxidizer/issues/541 is our tracking issue -for flat packages writing support. - -Extra Signing or Time-Stamp Token Operations -============================================ - -Signatures often need to encapsulate the size of the resulting signature. -This creates a chicken-and-egg problem because how can we know the size of -the resulting signature before we actually produce it! - -In some cases, this tool will create a *fake* signature and obtain an -actual time-stamp token from a server in order to resolve the size of -the data so we can better estimate the size of the real signature. - -We are not sure if Apple's tooling does this. But ours does and the -extra operations can be annoying because they may require extra unlocks -of signing keys or communications with a time-stamp token server. - -We can likely eliminate the extra use of the signing key for generating -these stand-in signatures and we can probably only make 1 request to the -time-stamp token server to obtain the size of its signatures. But we -haven't implemented this throughout the code base yet. - -https://github.com/indygreg/PyOxidizer/issues/542 and -https://github.com/indygreg/PyOxidizer/issues/543 track improvements here. - -Long Tail of Random Discrepancies from Apple's Tooling -====================================================== - -Apple's code signature format is really, really complex. There are tons of -data structures and fields with complex values. - -There is likely a long tail of minor differences in implementation that -result in variations between the behavior of our implementation and Apple's. - -In general, we consider differences in behavior in our implementation to -be bugs worth filing. Please follow the instructions at -:ref:`apple_codesign_debugging` to file GitHub issues with meaningful -details to debug the differences! - -Known areas where discrepancies are likely include: - -* The *code requirements* expression embedded into Mach-O binaries. We attempt - to derive one based on the signing key. The expression may not be exactly what - Apple's tools derive automatically. We consider this a bug. -* Executable segment flags and code signing flags. The exact logic for - determining what flags to set when is complex. In general, we consider - differences in behavior here to be bugs. -* Size of embedded signatures. You often need to estimate the size of the produced - embedded signature before signing because the signature encapsulates its own - size. Our estimation method varies from Apple's and can result in signatures - with more or less padded null bytes. This difference should be mostly harmless. - Improvements to make our signatures use fewer wasteful extra padding are - appreciated. diff --git a/apple-codesign/docs/apple_codesign_rcodesign.rst b/apple-codesign/docs/apple_codesign_rcodesign.rst deleted file mode 100644 index 42dfcf760..000000000 --- a/apple-codesign/docs/apple_codesign_rcodesign.rst +++ /dev/null @@ -1,86 +0,0 @@ -.. _apple_codesign_rcodesign: - -=================== -Using ``rcodesign`` -=================== - -The ``rcodesign`` executable provided by this project provides a command -mechanism to interact with Apple code signing. - -Signing with ``sign`` -===================== - -The ``rcodesign sign`` command can be used to sign a filesystem -path. - -Unless you want to create an ad-hoc signature on a Mach-O binary, you'll -need to tell this command what code signing certificate to use. - -To sign a Mach-O executable:: - - rcodesign sign \ - --p12-file developer-id.p12 --p12-password-file ~/.certificate-password \ - --code-signature-flags runtime \ - path/to/executable - -To sign an ``.app`` bundle (and all Mach-O binaries inside):: - - rcodesign sign \ - --p12-file developer-id.p12 --p12-password-file ~/.certificate-password \ - path/to/My.app - -To sign a DMG image:: - - rcodesign sign \ - --p12-file developer-id.p12 --p12-password-file ~/.certificate-password \ - path/to/app.dmg - -To sign a ``.pkg`` installer:: - - rcodesign sign \ - --p12-file developer-id-installer.p12 --p12-password-file ~/.certificate-password \ - path/to/installer.pkg - -Notarizing and Stapling -======================= - -You can notarize a signed asset via ``rcodesign notary-submit``. - -Notarization requires an App Store Connect API Key. See -:ref:`apple_codesign_app_store_connect_api_key` for instructions on how -to obtain one. - -Assuming you used ``rcodesign encode-app-store-connect-api-key`` to produce -a JSON file with all the API Key information, simply specify ``--api-key-path`` -to define the path to this JSON file. - -To notarize an already signed asset:: - - rcodesign notary-submit \ - --api-key-path ~/.appstoreconnect/key.json \ - path/to/file/to/notarize - -By default ``notarize-submit`` just uploads the asset to Apple. To wait -on its notarization result, add ``--wait``:: - - rcodesign notary-submit \ - --api-key-path ~/.appstoreconnect/key.json \ - --wait \ - path/to/file/to/notarize - -Or to wait and automatically staple the file if notarization was successful:: - - rcodesign notary-submit \ - --api-key-path ~/.appstoreconnect/key.json \ - --staple \ - path/to/file/to/notarize - -If notarization is interrupted or was initiated on another machine and you -just want to attempt to staple an asset that was already notarized, you -can run ``rcodesign staple``. e.g.:: - - rcodesign staple path/to/file/to/staple - -.. tip:: - - It is possible to staple any asset, not just those notarized by you. diff --git a/apple-codesign/docs/apple_codesign_remote_signing.rst b/apple-codesign/docs/apple_codesign_remote_signing.rst deleted file mode 100644 index 59b617cc1..000000000 --- a/apple-codesign/docs/apple_codesign_remote_signing.rst +++ /dev/null @@ -1,352 +0,0 @@ -.. _apple_codesign_remote_signing: - -=================== -Remote Code Signing -=================== - -This project has support for *remote signing*. This is a feature where -cryptographic signature operations (requiring access to the private key) -are delegated to a remote machine. - -From a high level, two machines establish a secure communications bridge with -each other through a central server. The *initiating* machine starts signing -operations like normal. But when it gets to an operation that requires producing -a cryptographic signature, it sends an end-to-end encrypted message to the -bound *signer* peer with the message to sign. The *signer* then uses its -private key to create a signature, which it sends back to the *initiator*, -who incorporates it into the code signature. - -Remote signing is essentially peer-to-peer, not client-server. The central -server exists for relaying encrypted messages between peers and not for -performing signing operations itself. Each signing *session* is ephemeral -and short-lived. Since the signing keys are offline by default and a human must -take action to join a signing session and use the signing keys, remote signing -is theoretically more secure than solutions like giving a (CI) machine -unlimited access to a code signing certificate or HSM. - -Remote signing is intended for use cases where the machine initiating signing -must not or can not have access to the private key material or unlimited -access to it. Popular scenarios include: - -* CI environments where you don't want a CI worker to have unlimited access - to the signing key because CI workers are notoriously difficult to secure. - (If someone can run arbitrary jobs on your CI they can likely exfiltrate any - CI secrets with ease.) -* When hardware security devices are used and machines initiating the signing - don't have direct access to this device. Think a remote CI machine or - coworker wanting to sign with a certificate in a YubiKey or HSM whose - access is entrusted to a specific person (or group of people in the case of - an HSM). - -.. important:: - - This feature is considered alpha and will likely change in future versions. - -.. danger:: - - The custom cryptosystem for remote signing has not yet undergone an audit. - The end-to-end message encryption and tampering resistance claims we've - made may be undermined by weaknesses in the design of the cryptosystem and - its implementation and interaction in code. - - In other words, use this feature at your own risk. - - `Issue 552 `_ tracks - performing an audit of this feature. - -How It Works -============ - -A full overview of the protocol and cryptography involved is available at -:ref:`apple_codesign_remote_signing_protocol` and you can read more about the -design and security at :ref:`apple_codesign_remote_signing_design`. - -From a high-level, signing operations involve 2 parties: - -* The *initiator* of the signing request. This is the entity that wants - something to be signed but doesn't having the signing certificate / key. -* The *signer*. This is the entity who has access to the private signing key. - -The signing procedure is essentially: - -1. *Initiator* opens a persistent websocket to a central server and publishes - details about that session and how to connect to it. -2. *Signer* follows the instructions from *initiator* and joins the *signing - session* by opening a websocket to the same server as the *initiator*. - Cryptography is employed to derive encryption keys so all subsequently - exchanged messages are end-to-end encrypted, preventing the server or any - privileged network actors from eavesdropping on signing operations or forging - a signing request. -3. *Initiator* sends a request to *signer* asking them to sign a message. -4. *Signer* inspects the request and issues a cryptographic signature, which it - sends back to *initiator*. -5. Steps 3-4 are repeated as long as necessary. - -Using -===== - -The *initiator* begins a remote signing *session* via ``rcodesign sign ---remote-signer``. (Some additional arguments are required - see below.) - -This command will print out an ``rcodesign`` command that the *signer* must -subsequently run to *join* the signing session. e.g.:: - - $ rcodesign sign --remote-signer --remote-shared-secret-env SHARED_SECRET - ... - connecting to wss://ws.codesign.gregoryszorc.com/ - session successfully created on server - Run the following command to join this signing session: - - rcodesign remote-sign gm1zaGFyZWRzZWNyZXQwg... - - (waiting for remote signer to join) - -At this point, that long opaque string - which we call a *session join string* - -needs to be copied or entered on the *signer*. e.g.:: - - $ rcodesign remote-sign --p12-file developer_id.p12 --remote-shared-secret-env SHARED_SECRET \ - gm1zaGFyZWRzZWNyZXQwg... - -If everything goes according to plan, the 2 processes will communicate with -each other and *initiator* will delegate all of its signing operations to -*signer*, who will issue cryptographic signatures which it sends back to the -*initiator*. - -Session Agreement -================= - -Remote signing currently requires that the *initiator* and *signer* exchange -and agree about *something* before signing operations. This ahead-of-time -exchange improves the security of signing operations by helping to prevent -signers from creating unwanted signatures. - -The sections below detail the different types of agreement and how they are -used. - -Public Key Agreement -==================== - -.. important:: - - This is the most secure and preferred method to use. - -In this operating mode, the *signer* possesses a private key that can decrypt -messages. When the *initiator* begins a signing operation, it encrypts a message -that only the *signer*'s private key can decrypt. This encrypted message is -encapsulated in the *session join string* exchanged between the *initiator* and -*signer*. - -This mode can be activated by passing one of the following arguments defining -the public key: - -``--remote-public-key`` - Accepts base64 encoded public key data. - - Specifically, the value is the DER encoded SubjectPublicKeyInfo (SPKI) - data structure defined by RFC 5280. - -``--remote-public-key-pem-file`` - The path to a file containing the PEM encoded public key data. - - The file can begin with ``-----BEGIN PUBLIC KEY-----`` or - ``-----BEGIN CERTIFICATE-----``. The former defines just the SPKI data - structure. The latter an X.509 certificate (which has the SPKI data - inside of it). - -Both the public key and certificate data can be obtained by running the -``rcodesign analyze-certificate`` command against a (code signing) certificate. - -The *signer* needs to use the corresponding private key specified by the -*initiator* in order to join the signing session. By default, ``rcodesign -remote-sign`` attempts to use the in-use code signing certificate for -decryption. - -So, an end-to-end workflow might look like the following: - -1. Run ``rcodesign analyze-certificate`` and locate the - ``-----BEGIN PUBLIC KEY-----`` block. -2. Save this to a file, ``signing_public_key.pem``. You can check this file into - source control - the contents aren't secret. -3. On the initiator, run ``rcodesign sign --remote-signer - --remote-public-key-pem-file signing_public_key.pem /path/to/input - /path/to/output``. -4. On the signer, run ``rcodesign remote-sign --smartcard-slot 9c - ````. - -We believe this method to be the most secure for establishing sessions because: - -* The state required to bootstrap the secure session is encrypted and can only - be decrypted by the private key it is encrypted for. If you are practicing - proper key management, there is exactly 1 copy of the private key and access - to the private key is limited. This means you need access to the private key - in order to compromise the security of the signing session. -* The session ID is encrypted and can't be discovered if the session join string - is observed. This eliminates a denial of service vector. - -Shared Secret Agreement -======================= - -.. important:: - - This method is less secure than the preferred *public key agreement* method. - -In this operating mode, *initiator* and *signer* agree on some shared secret -value. A password, passphrase, or some random value, such as a type 4 UUID. - -This mode is activated by passing one of the following arguments defining the -shared secret: - -``--remote-shared-secret-env`` - Defines the environment variable holding the value of a shared secret. - -``--remote-shared-secret`` - Accepts the raw shared secret string. - - This method is not very secure since the secret value is captured in plain - text in process arguments! - -An end-to-end workflow might look like the following: - -1. A secure, random password is generated using a password manager. -2. The secret value is stored in a password manager, registered as a CI secret, - etc. -3. The initiator runs ``rcodesign sign --remote-signer --remote-shared-secret-env - REMOTE_SIGNING_SECRET /path/to/input /path/to/output``. -4. The signer runs ``rcodesign remote-sign --remote-shared-secret-env - REMOTE_SIGNING_SECRET --smartcard-slot 9c``. - -Important security considerations: - -* Anybody who obtains the shared password could coerce the signer into signing - unwanted content. -* Weak password will undermine guarantees of secure message exchange and could - make it easier to decrypt or forge communications. - -Because the password exists in multiple locations, must be known by both -parties, and the process for generating it are not well defined, the overall -security of this solution is not as strong as the preferred *public key -agreement* method. However, this method is easier to use and may be preferred -by some users. - -.. _apple_codesign_remote_signing_github_actions: - -Using with GitHub Actions -========================= - -It is pretty simple to initiate remote code signing from GitHub Actions! In -fact, this scenario is one of the primary use cases for the design of the -feature. - -.. note:: - - `Issue #553 `_ tracks - publishing a canonical GitHub Action that formalizes the steps in this - documentation. Assistance in building that would be greatly appreciated! - -Here are the general steps. - -Configuring a Workflow / Actions --------------------------------- - -First, export the public key data of the signing certificate to a file -checked into source control. Use ``rcodesign analyze-certificate`` and -copy the ``-----BEGIN PUBLIC KEY----`` block to a file in your -repository. e.g. https://github.com/indygreg/PyOxidizer/blob/main/ci/developer-id-application.pem -defines the ``Developer ID Application`` public key data for the maintainer -of this project. - -.. note:: - - The public key data is included in the code signatures embedded in signed - artifacts so there is generally not a concern with making the public key - data widely available in the repository. - -Next, create a GitHub workflow or action that invokes ``rcodesign sign``. -https://github.com/indygreg/PyOxidizer/blob/main/.github/workflows/sign-apple-exe.yml -is an example of such a workflow. This particular workflow is using -``on.workflow_dispatch`` so the workflow is only triggered manually. See -the `workflow_dispatch documentation `_ -and `Manually running a workflow `_ -docs for more. - -.. important:: - - A manually triggered workflow is strongly recommended because a signer must - take manual action to perform remote signing and an automated trigger will - likely hang unless a person is around to attend to it. - -.. important:: - - For security reasons, you should set ``timeout-minutes`` on either the job - or step initiating remote signing to limit how long a signer will wait. - -The important steps in a remote signing action/workflow are: - -1. Securely obtain ``rcodesign``. We recommend downloading a release artifact - from https://github.com/indygreg/PyOxidizer/releases and pinning/verifying - the SHA-256 digest on download. -2. Download the artifact you want signed. The - `Download workflow artifact `_ - action can be useful for downloading artifacts from other workflows in the - current repository (since the official ``download-artifact`` action limits - you to artifacts in the current workflow). -3. Invoke ``rcodesign sign --remote-signer - --remote-public-key-pem-file path/to/public_key.pem``. -4. Do something with the signed result (like upload it as an artifact). - -Running the Workflow / Action ------------------------------ - -Now that you have a GitHub workflow or action in place, here's how you use it. - -If you followed the recommendations from above, the workflow is manually -triggered via ``on.workflow_dispatch``. You can trigger the workflow via -the GitHub web UI or via API. For API, the path of least resistance is likely -the ``gh`` `GitHub CLI `_ tool. e.g.:: - - gh workflow run sign-apple-exe.yml \ - --ref ci-main \ - -f workflow=rcodesign.yml \ - -f run_id=2214520041 \ - -f artifact=exe-rcodesign-macos-universal \ - -f exe_name=rcodesign - -If your workflow is highly parameterized (like this one), you may want to -script its invocation to make it more turnkey. - -When ``rcodesign sign --remote-signer`` runs in GitHub Actions, it will print -instructions on how to join the signing session. You will need to follow -these instructions in a timely manner to complete the code signing operation. - -Here is what you are looking for in the job output: - -.. image:: apple_codesign_actions_sjs_join.png - :alt: Screenshot of GitHub Actions run showing session join string - -Then, simply follow instructions on the machine with the signing key -to commence signing! - -.. important:: - - When you view the logs of a running GitHub Actions job, only the output - from after the point you started viewing them is visible. This means that - if you are *too late* you may not see the printed instructions for joining - the signing session! - - There are definitely some mitigations we can take for this. For the moment, - you need to be quick to open the job output in your browser. Or you can do - things like add a ``sleep`` before running ``rcodesign sign``. - -If all goes according to plan, you should see progress being printed -both in the signing process and from the near real time output from -GitHub Actions. - -Here is the output from the GitHub Actions (Linux) machine: - -.. image:: apple_codesign_actions_initiator_output.png - :alt: Signing output from GitHub Actions worker - -And from the signing Windows machine using a YubiKey for signing: - -.. image:: apple_codesign_actions_signer_output.png - :alt: Signing output from signing machine diff --git a/apple-codesign/docs/apple_codesign_remote_signing_design.rst b/apple-codesign/docs/apple_codesign_remote_signing_design.rst deleted file mode 100644 index ea87e326c..000000000 --- a/apple-codesign/docs/apple_codesign_remote_signing_design.rst +++ /dev/null @@ -1,195 +0,0 @@ -.. _apple_codesign_remote_signing_design: - -====================================================== -Remote Code Signing Design and Security Considerations -====================================================== - -Design Goals and Constraints -============================ - -The design of remote signing is influenced with the following primary goals in -mind: - -* The initiating machine MUST NOT have direct access to the private signing - key. Ever. The private key (or ability to create signatures with it) is only - ever in possession of the signer. -* The private key cannot be used without the signer's knowledge (and optional - consent to each use). -* The initiating machine must be able to run remotely / non-interactively. - -We also imposed the following constraints when considering designs: - -* The initiating machine is partially trusted. We assume that if you trust the - initiating machine to invoke a signing operation then you trust that machine - to e.g. not lie about the signing requests it subsequently presents to the - signer. -* We should place minimal trust in any 3rd party servers or machines. Assume - all 3rd parties are malicious and will attempt to coerce signers into signing - arbitrary content. -* 3rd party servers should have access to as little information about signing - activity as possible. e.g. 3rd party servers should not be able to observe - the messages that are signed, the produced signatures, or the certificates - used to sign. They may observe details that leak through side channels, such - as the number of messages exchanged and the sizes of encrypted ciphertexts. -* We assume the existence of an out-of-band side-channel for 2 peers to exchange - information at signing time. This means we require some synchronous activity - by the signer in order to fulfill signing requests. (The signer isn't just - running an always-running server that responds to signing requests.) - -Threat Models -============= - -The following threat models dictate some design choices: - -* A malicious brokering server or man-in-the-middle could coerce the signer into - signing unwanted content. -* A malicious 3rd party could disrupt signing operations by sending garbage - messages to the brokering server, either in general or directed at established - sessions. i.e. DoS against the server. -* A malicious brokering server or man-in-the-middle could fulfill signature - requests using the *wrong* certificate. - -If signing sessions were conducted without any prior knowledge of the peer, -neither peer would be able to trust or authenticate the other. You could -securely exchange end-to-end encrypted messages with a peer. But the *initiator* -wouldn't be able to answer the question *is this signed by who I want it to be -signed by*. And more importantly, the *signer* wouldn't be able to answer -*do I trust the initiator to send me content that I want to sign*. - -You can't establish a trust relationship without a trust anchor. **So in order -to establish trust we require that peers share pre-existing knowledge of the -other before signing operations.** The exact mechanism can vary. But *some* -pre-existing knowledge needs to be conveyed to the other peer in order to serve -as a trust anchor. - -Since all designs rule out the possibility of the private key being directly -accessed or used by the *initiator*, the next best attack vector is tricking -the *signer* into signing untrusted/malicious content. - -The easiest way to conduct this attack is for a malicious server or -man-in-the-middle to intercept communications and/or issue a malicious signing -request. There are a few mitigations for this. - -First, *signers* must have presence in order to create signatures. When signers -go offline, they can't produce signatures. So attacks against signers must occur -when the signer is online. - -Second, we employ end-to-end encryption of peer-to-peer messages using -ephemeral encryption keys unique to the session and logically derived from a -pre-existing trust anchor. A malicious 3rd party would need access to data -never transmitted in plaintext through the server in order to decrypt messages -or issue fake/malicious messages. - -Security Analysis in the Bigger Picture -======================================= - -When considering the overall security of remote code signing, we have to -consider the broader ecosystem in which it exists. - -Without remote code signing, the following are all commonly true: - -* Signing keys are copied to multiple machines to make it easier to access - them. -* Signing keys are made available as secrets on CI workers. -* Access to perform operations on the signing key is always on. e.g. - anybody who can talk to the HSM can create a signature. -* Security conscious people (those who want to minimize risk for private - keys) need to impose a more complicated release pipeline - one that - typically entails copying assets to a separate machine, signing them, - then copying elsewhere. These steps are often tedious and effectively - constitute a barrier to good security hygiene. - -There are general principles of private key management: - -* You should have as few copies of the private key as possible. Ideally 1. -* Keys should be as short lived as possible or access to them should be - limited in time duration. - -Traditional solutions to code signing violate these principles because -there's not an easy-to-use / viable alternative. So in the absence of -remote code signing, commonly practiced code signing key management is -generally not great. - -We believe that our design of remote code signing is intrinsically more -secure than what is commonly practiced because: - -* The signer in possession of the private key must be present. There is - no unlimited access to the private key outside an active signing session. -* You can have exactly 1 copy of the private key without compromising on - usability. The urge to make copies to streamline CI/CD is largely mitigated - via an easy-to-use remote signing UI. - -In addition, the design and implementation of the relay server further -bolsters security by: - -* Purging sessions after a maximum time to live (measured in minutes). -* Refusing to allow N>2 peers from sending messages to a session. -* Requiring active presence for message exchange. The server doesn't store - a copy of relayed signing messages so there isn't a potential for someone - to deposit a malicious message for later retrieval. - -And these security properties are delivered without even factoring in -end-to-end message encryption! The end-to-end encryption is effectively -protections against a malicious server or man-in-the-middle. These are -arguably necessary protections - especially when using a server hosted by -an (untrusted) 3rd party. But for scenarios where you run your own server -and you trust the network, end-to-end encryption isn't buying you much beyond -what signer presence requirements and server design already deliver. - -Default Remote Code Signing Server -================================== - -By default, this project uses the remote code signing server at -``wss://ws.codesign.gregoryszorc.com/``. - -This service is operated by the maintainer of this project and is provided -for free for use by the community. However, there is no formal or legal -agreement around the availability of its service or its operation. - -The service is hosted on AWS and uses API Gateway + Lambda + DynamoDB -and should be highly reliable, as these services rarely experience outages. - -The :ref:`apple_codesign_remote_signing_protocol` and implementation of the -server have been purposefully designed to be respectful of privacy of its -users. - -Meaningful messages between clients are end-to-end encrypted and the server -is unable to determine the contents of those messages. The server only has -access to protocol-level details, such as which APIs are being invoked and -the sizes of the payloads. - -The server does have access to client IPs and any additional metadata -in HTTP requests and websocket frames. However, IPs or other identifying -information is not read by our custom code powering the websocket server or -retained in any logs to the best of our knowledge. (We believe user data -to be toxic and don't want anything to do with it.) - -Some metrics to monitor the health of the service and help prevent abuse -are recorded. These include the counts of different API invocations and -the sizes of message payloads. - -The code powering the server and the Terraform for deploying it on AWS -are open source and available to audit. See -:ref:`apple_codesign_remote_signing_running_your_own_server` for details. -Of course, there's no way to prove that ``ws.codesign.gregoryszorc.com`` -is running the same configuration as the provided open source code. You -*just* have to trust that the maintainer of this project values the privacy -of his users. - -.. _apple_codesign_remote_signing_running_your_own_server: - -Running Your Own Server -======================= - -If you are unable or unwilling to use the default remote signing server -operated by the maintainer of this project, it is possible to deploy your -own server instance. - -The source code for the server and a Terraform module for deploying it into -AWS are available in this repository in the -``terraform-modules/remote-code-signing`` directory. The canonical location -is https://github.com/indygreg/PyOxidizer/tree/main/terraform-modules/remote-code-signing. - -See its README for instructions on how to use. Once deployed at a different -hostname, you'll need to provide the ``--remote-signing-url`` argument to -relevant commands to override the default signing server URL. diff --git a/apple-codesign/docs/apple_codesign_remote_signing_protocol.rst b/apple-codesign/docs/apple_codesign_remote_signing_protocol.rst deleted file mode 100644 index 4cd3e1f81..000000000 --- a/apple-codesign/docs/apple_codesign_remote_signing_protocol.rst +++ /dev/null @@ -1,836 +0,0 @@ -.. _apple_codesign_remote_signing_protocol: - -============================ -Remote Code Signing Protocol -============================ - -Overview -======== - -The remote signing protocol facilitates the cryptographic signing of messages -involving 2 discrete network peers. - -The peer that wants something signed is the **initiator**. - -The peer with access to the signing key that produces cryptographic -signatures is the **signer**. - -Peers establish persistent websocket connections to a central server to -enable them to speak with each through firewalls and NATs. - -Peers register an ephemeral *session* with the server, which is essentially -a binding between 2 connected websocket clients. - -Peers derive session-specific encryption keys using mutually agreed upon -ahead of time data. They then relay end-to-end encrypted messages through -the central server and perform cryptographic signing operations. - -Wire Protocol -============= - -The protocol entails the exchange of JSON encoded objects via websockets. - -The JSON objects sent from clients to the server have the following keys: - -``request_id`` - (string) (required) A unique identifier for this request. - -``api`` - (string) (required) The name of the API / method to invoke on the server. - -``payload`` - (object) (optional) Parameters passed to this API invocation. - -The JSON objects sent from servers to clients have the following keys: - -``request_id`` - (string) (optional) Echo of ``request_id`` from the message that generated - this one. The value could be unknown to the receiver if this message was - generated from the other peer in the session. - -``type`` - (string) (required) The message type. - -``ttl`` - (number) (optional) Integer number of seconds remaining before the session - expires and will be automatically deleted by the server. - -``payload`` - (object) (optional) Payload further describing this message. - -All other fields in the top-level object are reserved for future use. - -Messages sent from the client to server ALWAYS result in the server responding -to that API request. - -It is also possible for servers to send messages to clients asynchronously -of any client-initiated message. - -Initial Connection Protocol -=========================== - -When a client connects to the server, it SHOULD issue a ``hello`` API -message and wait for the server's response. - -If the response contains a *message of the day* string, it MUST be displayed -to the end-user. - -Clients SHOULD also make a best effort attempt to validate the server's -advertised capabilities and make a determination about compatibility and -error or print warnings if incompatibility is detected. - -.. _apple_codesign_remote_signing_sessions: - -Session Negotiation -=================== - -The *initiator* and *signer* pair with each other by forming a *session*. - -From the server's perspective, a *session* is an opaque identifier string -with associated state, such as the unique websocket connection IDs of the -*initiator* and *signer* clients. - -Sessions are ephemeral and expire automatically after a duration specified -by the initiating client. (The server can impose a maximum duration to prevent -service abuse.) - -Sessions are generally created by the *initiator*. - -The *initiator* creates a unique session ID, ``SessionId``. ``SessionId`` MUST -be randomly chosen. It SHOULD have sufficient entropy to prevent server-side -collisions. The use of type 4 UUIDs for session IDs is recommended. - -Once a server-side session is created, the *initiator* then shares a -*session join string* with the signer via an out-of-band mechanism. -See :ref:`apple_codesign_remote_session_join_strings` for more. - -At this point, mechanisms diverge based on the session joining mechanism -employed. But generally speaking, the *signer* sends a -:ref:`apple_codesign_remote_api_client_join_session` to the server -to register itself as the other peer in the session. At this point, both -peers derive encryption keys and communicate with each other by issuing -:ref:`apple_codesign_remote_api_client_send_message` messages. See -:ref:`apple_codesign_remote_signing_protocol_encrypted_protocol` for more. - -.. _apple_codesign_remote_session_join_strings: - -Session Join Strings -==================== - -The *initiator* and *signer* need to leverage an out-of-band mechanism for -communicating metadata with each other in order to join a server-established -session. There are various potential solutions for this and we've purposefully -designed the mechanism to be extensible. - -Generically, the mechanism to join a session is expressed through a -**session join string**, or SJS. - -The SJS is ultimately a CBOR encoded array of length 2. The array's elements -are: - -* (string) The scheme being used. -* (varied) The payload for that scheme. - -But to end-users it is an opaque string. - -The SJS can be encoded as: - -* Base64 using the RFC 3548 *URL safe* character set with optional ``=`` - padding. -* PEM using ``SESSION JOIN STRING`` as the armoring tag. - -In general, the *session join string* is shared out-of-band with the other -peer, who uses it to join the session. - -In general, *session join strings* are designed such that a 3rd party -becoming aware of the SJS will not jeopardize the security of the current or -future signing operations. However, denial of service could occur if the SJS -exposes the session ID and a 3rd party joins the session before the *intended* -peer. - -The following sections denote the defined *session join string* schemes. -Sections names are the ``scheme`` value. - -``publickey0`` --------------- - -The ``publickey0`` session joining mechanism relies on public key cryptography -to authenticate the 2nd peer in a session by leveraging knowledge of the -2nd peer's public encryption key. - -The initiating peer, ``A``, MUST know the public key of the joining peer, -``B``. - -``A`` generates a random value at least 32 bytes long, ``ChallengeSecret``. - -``A`` generates a new RFC 7748 Curve 25519 private key. Its private / -public components are ``AAgreementPrivate`` and ``AAgreementPublic``, -respectively. - -``A`` generates a new random 16 byte value, ``SharedAESKey``. - -``A`` loads the public key of ``B``, ``BPublic``. It usually does so by -extracting the X.509 SubjectPublicKeyInfo (SPKI) (RFC 5280 Section 4.1.2.7) -from an X.509 certificate or DER/PEM fragment of just the SPKI. - -``A`` prepares a plaintext message to be sent to ``B``, ``AJoinPlaintext``. -This message is a CBOR array with the following elements: - -``serverUrl`` - (Index 0) (optional string) URL of the server to connect to. - -``sessionId`` - (Index 1) (string) The session identifier created on the server. - -``challenge`` - (Index 2) (bytes) The content of ``ChallengeSecret``. - -``agreementPublic`` - (Index 3) (bytes) ``SubjectPublicKeyInfo`` for ``AAgreementPublic``. - -``A`` encrypts ``AJoinPlaintext`` using AES-128 in GCM with ``SharedAESKey``, -yielding ``AJoinCiphertext``. A 12 byte nonce is used where the bytes are all -``0x42``. The 16 byte authentication tag is appended to the raw ciphertext -and constitutes the final bytes of ``AJoinCiphertext``. - -``A`` encrypts ``SharedAESKey`` using asymmetric encryption targeting -``BPublic``, yielding ``SharedAESCiphertext``. - -For RSA, OAEP padding with SHA-256 digests MUST be used. - -The payload of the *session join string* is a CBOR array with the following -elements: - -``aes_ciphertext`` - (Index 0) (bytes) The ``SharedAESCiphertext`` generated above. - -``bPublic`` - (Index 1) (bytes) The SPKI describing which public key was used to - encrypt ``SharedAESCiphertext``. - -``message_ciphertext`` - (Index 2) (bytes) The ``AJoinCiphertext`` generated above. - -So, the final *session join string* is -``["publickey0", [SharedAESCiphertext, BSPKI, AJoinCiphertext]]``. - -The *session join string* is summarily CBOR and base64 encoded and made -available to ``B``. - -``B`` receives and decodes the SJS. - -``B`` locates the decryption key from the provided SPKI structure. (``B`` -may want to impose restrictions here to prevent clients from fishing for -specific keys.) - -``B`` decrypts ``SharedAESCiphertext`` using ``BPrivate``, yielding back -``SharedAESKey``. - -Using ``SharedAESKey``, ``B`` verifies and decrypts ``AJoinCiphertext``, -yielding ``AJoinPlaintext``. - -On success, ``B`` generates a new RFC 7748 Curve 25519 private key, -``BAgreementPrivate`` and ``BAgreementPublic``. - -``B`` connects to the server and sends a -:ref:`apple_codesign_remote_api_client_join_session` message with ``context`` -set to ``BAgreementPublic``. - -At this point, ``A`` and ``B`` both perform key agreement using their -ephemeral ED25519 private key and the public key of the other peer, each -mutually deriving ``SessionSharedKey``. - -At this point, the procedure described in -:ref:`apple_codesign_remote_signing_aead_keys` is used to derive new symmetric -encryption keys. ``ChallengeSecret`` is used as the additional value to -derive ``IdentifierA`` and ``IdentifierB``. - -Security Considerations -^^^^^^^^^^^^^^^^^^^^^^^ - -The *session join string* consists of 2 discrete encrypted payloads and is -generally safe against offline attacks. Unless ciphers are broken, the -private key is required to obtain for anything beyond side-channels (like -total payload size). - -``SessionId`` is encrypted, so compromise of the SJS can't easily lead to a -DoS by an unwanted peer joining the session. - -The server doesn't see anything: the encrypted AES key and AES encrypted -peer metadata are both encapsulated in the SJS. We could potentially move -some of these to the server to reduce the length of the SJS. - -Open Questions for Security Audit -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* We don't sign / HMAC the asymmetrically encrypted AES key. Nor do we - include an IV or other prepended message. This seems to go against - best practices. Does it matter? Does the additional layer of AEAD feeding - into the key agreement compensate for this? -* Is the use of a constant nonce for the ``SharedAES`` -> ``AJoinCiphertext`` - acceptable? The AES key is randomly generated and is used exactly once, so - do the nonces even matter? -* Is AES-128 in GCM mode a sufficient key/cipher for encrypting the main - message? -* We currently generate 2 distinct private keys: 1 for key agreement and 1 - for AES encryption. They are generated independently. Does this make sense - or should perhaps HKDF be used against a common key? -* Right now there is no explicit trust anchoring between the asymmetric - encryption targeting ``B`` and the derived shared secret key. Should ``B`` - produce a cryptographic signature using ``BPrivate`` so ``A`` doesn't assume - that *ability to decrypt* authenticates ``B``? Or is *ability to decrypt* - along with the assumption that only ``B`` possesses ``agreementPublic`` - sufficient? - -``sharedsecret0`` ------------------ - -The ``sharedsecret0`` session joining mechanism uses SPAKE2 to derive a shared -encryption key using an ahead-of-time mutually agreed upon shared secret, -``SharedSecret``. - -The peer creating the session, henceforth ``A``, generates unique/random -``SessionId`` and ``Identifier`` values. These values are used to construct -the SPAKE2 identifier strings: ``A:{SessionId}:{Identifier}`` and -``B:{SessionId}:{Identifier}``. - -``A`` begins SPAKE2 role A initialization using ``SharedSecret`` and role A's -identifier string. This produces ``SpakeAInit``. - -``A`` calls :ref:`apple_codesign_remote_api_client_create_session` to -register the new session with the server. Its ``context`` field is empty. - -The *session join string* value is a CBOR array with the following elements: - -``sessionId`` - (Index 0) (string) The session identifier string. - -``identifier`` - (Index 1) (bytes) The random ``Identifier`` value produced earlier. - -``spakeAInit`` - (Index 2) (bytes) The SPAKE2 Role A initialization message. - -The final CBOR *session join string* is -``["sharedsecret0", [SessionId, Identifier, SpakeAInit]]``. - -The *session join string* is summarily CBOR and base64 encoded and made -available to ``B``. - -``B`` receives and decodes the SJS. - -``B`` performs SPAKE2 Role B initialization, producing ``SpakeBInit``. - -``B`` sends a :ref:`apple_codesign_remote_api_client_join_session` message -to the server with ``context`` set to the base64 encoding of ``SpakeBInit``. -``SpakeBInit`` is relayed to ``A`` via the server. - -At this point, both ``A`` and ``B`` are able to finalize SPAKE2 using -``SpakeBInit`` and ``SpakeAInit``, respectively. They should mutually derive -a shared encryption key, ``SessionSharedKey``. - -At this point, the procedure described in -:ref:`apple_codesign_remote_signing_aead_keys` is used to derive new symmetric -encryption keys. ``Identifier`` is used as the additional value used to -derive ``IdentifierA`` and ``IdentifierB``. - -Security Considerations -^^^^^^^^^^^^^^^^^^^^^^^ - -The *session join string* containing the plaintext ``SessionId``, -``Identifier``, and ``SpakeAInit`` generally does not need to be highly -secure or made secret. - -``SharedSecret`` cannot be derived from knowledge of the *session join string*. - -The server does not directly observe the value for ``Identifier``, only -``SpakeBInit``. So it would need knowledge of the *session join string* -and ``SharedSecret`` to decrypt messages. - -A 3rd party in a privileged network position (including the server) with -knowledge of ``SharedSecret``, ``SessionId``, and ``Identifier`` would be -able to decrypt and forge messages, as it would be able to derive ``RoleAKey`` -and ``RoleBKey``. So it is important to use transport-level encryption, -a trusted server, and keep ``SharedSecret`` a secret value. - -Open Questions for Security Audit -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* Is SPAKE2 the best mechanism for deriving session encryption keys from a - shared secret? -* Should ``SpakeAInit`` be in the *session join string* or stored on the server - and hidden from plaintext view? What are the tradeoffs with each approach? -* As proposed, the SPAKE2 identifier contains ``SessionId`` and yet another - random value. That random value is not sent to the server but is possibly - world readable in the *session join string*. Is this second source of entropy - necessary? Does attempting to prevent the server from having access to it buy - us any security value? Or is just the client-chosen ``SessionId`` string good - enough? -* The SPAKE2 specification seems to insist on the use of key confirmation - messages. Since we're using HKDF into AEAD, which has built-in authentication, - do we need to perform the SPAKE2 key confirmation since any failures in SPAKE2 - land would lead to AEAD failures anyway? -* How sensitive is SPAKE2 to the entropy of ``SharedSecret``? While we want to - encourage a relatively strong ``SharedSecret``, we can't guarantee this. - Should we be doing e.g. PBKDF2 on ``SharedSecret`` before feeding it into - SPAKE2 or will SPAKE2 do sufficient *key stretching* on its own? - -.. _apple_codesign_remote_signing_aead_keys: - -AEAD Key Derivation -------------------- - -The schemes above commonly detail the steps to enable 2 peers to mutually -derive a session-ephemeral shared encryption key, ``SessionSharedKey``. - -Rather than use ``SessionSharedKey`` directly for subsequent message exchange, -we instead derive additional keys from it for use with Authenticated Encryption -and Additional Data (AEAD) encryption / message exchange. - -An identifier value is associated with peers assuming roles ``A`` (the session -initiator) and ``B`` (the session joiner). The value is a bytes concatenation -of: - -* The role name. e.g. ``A`` / ``0x41`` or ``B`` / ``0x42``. -* A colon (``:`` / ``0x3a``) -* The ``SessionId`` identifier, UTF-8 encoded. -* A colon (``:`` / ``0x3a``) -* An additional value communicated in the session join string. e.g. - ``ChallengeSecret``. - -These values are known as ``IdentifierA`` and ``IdentifierB``. - -HKDF is used to derive new keys. - -Step 1 / HKDF-Extract uses an empty salt and ``SessionSharedKey`` to produce -a pseudorandom key, ``PRK``. - -Step 2 / HKDF-Expand is performed twice to derive 2 new keys. The first -invocation uses ``IdentifierA`` for ``info`` and ``32`` for ``L``, producing -``RoleAKey``. The second invocation uses ``IdentifierB`` for ``info`` and ``32`` -for ``L``, producing ``RoleBKey``. - -``RoleAKey`` and ``RoleBKey`` are used to empower AEAD encryption / message -exchange. ChaCha20+Poly1305 is used. Nonces are 12 bytes where the first 4 -bytes are a little-endian u32 counter whose initial used value is ``0`` and -the subsequent 8 bytes are always ``0``. Additionally authenticated data -(``AAD``) is generally not used. - -``RoleAKey`` is used by ``A`` to encrypt messages and by ``B`` to -verify/decrypt messages from ``A``. ``RoleBKey`` is used by ``B`` to -encrypt messages and by ``A`` to verify/decrypt messages from ``B``. - -Open Questions for Security Audit -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* Is ChaCha20+Poly1305 a reasonable cipher choice? Or should we be using - block ciphers (e.g. AES)? -* Using a simple, easily guessable counter for nonces seems wrong. Using a - random value seems more appropriate. But both parties need to know what the - nonce we be. Do we use a random value for the nonce but encode the nonce in - plaintext next to the exchanged ciphertext messages? Or do we need something - else entirely? -* We could potentially use additionally authenticated data (AAD) to encapsulate - more details of the request, such as the request ID. Does that buy us - security benefits? - - -.. _apple_codesign_remote_signing_protocol_encrypted_protocol: - -Signing Protocol -================ - -Once 2 peers have established a session and derived encryption keys to -facilitate end-to-end encrypted communication, they communicate with each -other using :ref:`peer to peer messages ` -by invoking the :ref:`apple_codesign_remote_api_client_send_message` API. - -This process generally involves a handshake: - -1. Both peers simultaneously send :ref:`apple_codesign_remote_api_peer_ping` - messages. -2. Upon receipt, each peer sends a :ref:`apple_codesign_remote_api_peer_pong` - in response. This dance confirms peer presence and that the derived - encryption keys work. -3. The *initiator* sends a - :ref:`apple_codesign_remote_api_peer_request_signing_certificate` to request - information about the signer's public certificate. This is necessary in - order to allow the signer to do things like estimate the sizes of signatures - and to derive additional details needed for signing. -4. The *signer* sends a - :ref:`apple_codesign_remote_api_peer_signing_certificate` in response. - -At this point, both peers are ready to commence signing. - -5. The *initiator* sends a :ref:`apple_codesign_remote_api_peer_sign_request`. -6. The *signer* receives the request, assesses it, creates a cryptographic - signature, and sends a :ref:`apple_codesign_remote_api_peer_signature` - in reply. -7. Steps 5-6 are repeated as necessary. - -Finally, - -8. Either peer sends a :ref:`apple_codesign_remote_api_client_goodbye` to - finalize the session. - -Client Issued Messages -====================== - -The following sections denote the types of messages issued from clients to -servers. - -Section names denote the value of the ``api`` key in the messages. - -.. _apple_codesign_remote_api_client_hello: - -``hello`` ---------- - -Greets the server and obtains information about the server. - -This message type has no payload. - -Servers respond to this message with a -:ref:`apple_codesign_remote_api_server_greeting`. - -.. _apple_codesign_remote_api_client_create_session: - -``create-session`` ------------------- - -Requests the creation of a new session on the server. - -Sent by the *initiator* as part of session negotiation. - -Fields: - -``session_id`` - (string) (required) Unique identifier to use for this session. - -``ttl`` - (number) (required) Requested session duration, in seconds. - -``context`` - (string) (optional) Additional context to be passed to the peer when it - joins the session. - -Servers SHOULD automatically expire the server-side session state after its -TTL duration expires. Servers MAY close connections to connected clients when -their session expires. Servers MAY impose a shorter TTL if the requested TTL -is too long. - -Servers respond to this message with a -:ref:`apple_codesign_remote_api_server_session_created`. - -.. _apple_codesign_remote_api_client_join_session: - -``join-session`` ----------------- - -Attempts to join an existing session. - -Sent by the *signer* as part of session negotiation. - -Fields: - -``session_id`` - (string) (required) Identifier of session to join. - -``context`` - (string) (optional) Additional context to pass through to the other - peer. - -Servers respond to this message with a -:ref:`apple_codesign_remote_api_server_session_joined`. - -.. _apple_codesign_remote_api_client_send_message: - -``send-message`` ----------------- - -Sends an (encrypted) message to the other peer in this session. - -Fields: - -``session_id`` - (string) (required) Identifier of session to use for peer lookup. - -``message`` - (string) (required) Base64 encoded ciphertext of an AEAD encrypted - message to send to the peer. - -Server implementations MUST ensure that the client issuing this request -are bound to the session they are attempting to send a message to. - -Servers react to this message by sending a -:ref:`apple_codesign_remote_api_server_peer_message` to the other peer -in the specified session. - -Servers respond to this message with a -:ref:`apple_codesign_remote_api_server_message_sent`. - -.. _apple_codesign_remote_api_client_goodbye: - -``goodbye`` ------------ - -Indicates the client is finished and will be disconnecting. - -Fields: - -``session_id`` - (string) (required) Identifier of session to use for peer lookup. - -``reason`` - (string) (option) Reason the client is disconnecting. - -Server implementations MUST ensure that the client issuing this request -is bound to the session they are attempting to close. - -Servers react to this message by sending a -:ref:`apple_codesign_remote_api_server_session_closed` to the other peer -in the specified session. - -Servers respond to this message with a -:ref:`apple_codesign_remote_api_server_session_closed`. - -Server Sent Messages -==================== - -The following sections denote the types of messages sent from the server -to clients. - -Section names denote the value of the ``type`` field in the message. - -.. _apple_codesign_remote_api_server_greeting: - -``error`` ---------- - -Conveys information about a server-side error. - -Could be sent in reply to any API request or sent asynchronously if some -error occurred (such as the peer disconnecting unexpectedly). - -Fields: - -``code`` - (string) (required) Value that uniquely identifies this error type. - -``message`` - (string) (required) Human readable error message. - -``greeting`` ------------- - -Conveys information about the server. - -Sent in reply to a :ref:`apple_codesign_remote_api_client_hello` request. - -Fields: - -``apis`` - (array of strings) (required) Names of APIs that the server supports. - -``motd`` - (string) (optional) *Message of the day* conveying messaging that the - server operator wishes clients to know about. - -.. _apple_codesign_remote_api_server_session_created: - -``session-created`` -------------------- - -Conveys the successful creation of a session. - -Sent in reply to a :ref:`apple_codesign_remote_api_client_create_session` -request. - -.. _apple_codesign_remote_api_server_session_joined: - -``session-joined`` ------------------- - -Conveys the successful joining into a session. - -Sent in reply to a :ref:`apple_codesign_remote_api_client_join_session` -request. - -Sent asynchronously by servers in response to a -:ref:`apple_codesign_remote_api_client_join_session` issued by the joining -peer. - -Fields: - -``context`` - (string) (optional) Data from the peer required to finish initializing - the session. - - If this message was sent in reply to a - :ref:`apple_codesign_remote_api_client_join_session`, the value will be - from the initiating peer. - - If this message was sent to the pre-existing peer in reaction to a - :ref:`apple_codesign_remote_api_client_join_session`, the value will be - from the joining peer. - -.. _apple_codesign_remote_api_server_message_sent: - -``message-sent`` ----------------- - -Conveys the successful sending of a message to the session peer. - -Sent in reply to a :ref:`apple_codesign_remote_api_client_send_message` -request. - -.. _apple_codesign_remote_api_server_peer_message: - -``peer-message`` ----------------- - -Delivers an (encrypted) message from the peer in this session. - -Sent asynchronously by servers in response to a -:ref:`apple_codesign_remote_api_client_send_message` issued by the -other peer in a session. - -Fields: - -``message`` - (string) (required) Base64 encoded AEAD message. - -.. _apple_codesign_remote_api_server_session_closed: - -``session-closed`` ------------------- - -Conveys that the session has been finalized and can no longer be used. - -Sent in reply to a :ref:`apple_codesign_remote_api_client_goodbye` request -as well as asynchronously to the peer in its session. - -Fields: - -``reason`` - (string) (optional) Provides further context on why the session was closed. - -.. _apple_codesign_remote_api_peer_messages: - -Peer to Peer Messages -===================== - -Peers within a session communicate with each other by sending and receiving -:ref:`apple_codesign_remote_api_client_send_message` and -:ref:`apple_codesign_remote_api_server_peer_message`, respectively. - -The ``message`` field denotes a base64 encoded AEAD encrypted message. The -message consists of the ciphertext with the authentication tag appended. The -plaintext of these messages is the JSON encoding of an object having the -following keys: - -``type`` - (string) (required) The message type. This is unique message namespace from - server-sent messages. - -``payload`` - (object) (optional) Payload for this message. - -The following sections denote the types of peer-to-peer messages. The section -names denote the value for the ``type`` field. - -.. _apple_codesign_remote_api_peer_ping: - -``ping`` --------- - -Check on the status of the peer. - -Receivers should send a :ref:`apple_codesign_remote_api_peer_pong` in response. - -.. _apple_codesign_remote_api_peer_pong: - -``pong`` --------- - -Respond to a status check from a peer. - -Sent in response to a :ref:`apple_codesign_remote_api_peer_ping` message. - -.. _apple_codesign_remote_api_peer_request_signing_certificate: - -``request-signing-certificate`` -------------------------------- - -Requests the peer to send it information about its signing certificate. - -Receivers should send a -:ref:`apple_codesign_remote_api_peer_signing_certificate` in response. - -Should only be sent by the *initiator*. - -.. _apple_codesign_remote_api_peer_signing_certificate: - -``signing-certificate`` ------------------------ - -Describes the signing certificate(s) that is being used by the signer. - -Sent in response to a -:ref:`apple_codesign_remote_api_peer_request_signing_certificate`. - -Fields: - -``certificates`` - (array of object) (required) Contains a list of signing certificates that - will potentially be used. - - Each entry is an object described below. - - Today, there is likely a single certificate in this array. We've - left the door open for supporting the use of multiple signing - certificates in the future. - -Each entry in the ``certificatess`` array is an object with the following -fields: - -``certificate`` - (string) (required) Base64 encoded DER of the public X.509 certificate. - -``chain`` - (array of strings) (optional) Base64 encoded DER of additional public - X.509 certificates in the signing chain for this certificate. - -.. _apple_codesign_remote_api_peer_sign_request: - -``sign-request`` ----------------- - -Requests the cryptographic signing of a message. - -Fields: - -``message`` - (string) (required) Base64 encoded message to be signed. - -.. _apple_codesign_remote_api_peer_signature: - -``signature`` -------------- - -Conveys the cryptographic signature over a message. - -Sent in response to a -:ref:`apple_codesign_remote_api_peer_sign_request`. - -Fields: - -``message`` - (string) (required) Base64 encoded message that was signed. - -``signature`` - (string) (required) Base64 encoded signature data. - -``algorithm_oid`` - (string) (required) Base64 encoded DER encoding of OID denoting the - signature algorithm. diff --git a/apple-codesign/docs/apple_codesign_smartcard.rst b/apple-codesign/docs/apple_codesign_smartcard.rst deleted file mode 100644 index d29ee2e04..000000000 --- a/apple-codesign/docs/apple_codesign_smartcard.rst +++ /dev/null @@ -1,161 +0,0 @@ -.. _apple_codesign_smartcard: - -================== -Smart Card Support -================== - -This project has some support for integrating with Smart Cards. This -enables you to perform cryptographic signing using a certificate that -is stored in a hardware device. - -Certificates stored this way are more secure, as it typically requires -that a physical device be unlocked in order to use the private key. And -access to the raw private key matter is typically not allowed. - -Cargo Feature -============= - -Smart card integration requires the optional and disabled-by-default -``smartcard`` Cargo feature to be enabled. - -On macOS and Windows, this feature should *just work*. - -On Linux, you'll need a package providing ``pcsclite`` installed or you may -get a cryptic build error due to missing dependencies. On Debian based distros, -you want to ``apt install libpcsclite1 libpcsclite-dev`` (or something of that -nature). - -Limitations -=========== - -We currently use `yubikey.rs `_ for -smart card integration. This likely means that only YubiKeys currently work. - -However, we would like to switch to a more generic interface (such as -`pcsc `_ in the future to allow more flexible -usage. - -There is currently no support for setting the management key. If you have -set a custom management key, you won't be able to import certificates onto -your smart card. However, signing should still work. - -Validating Smart Card Integration -================================= - -To see if your smart card device is recognized and certificates can be found:: - - $ rcodesign smartcard-scan - Device 0: Yubico YubiKey OTP+FIDO+CCID 0 - Device 0: Serial: 12345678 - Device 0: Version: 5.2.7 - Device 0: Certificate in slot Signature / 9c - Subject CN: gps - Issuer CN: gps - Subject is Issuer?: true - Team ID: - SHA-1 fingerprint: c847e830c01845517d7e3775805ab56313aa11c8 - SHA-256 fingerprint: 7c0bc8fe1a2d7831ca0b0787dc6d5c28c6f562c2723a7eaaab42d39e7a3b7924 - Signed by Apple?: false - Guessed Certificate Profile: none - Is Apple Root CA?: false - Is Apple Intermediate CA?: false - Apple CA Extension: none - Apple Extended Key Usage Purpose Extensions: - Apple Code Signing Extensions: - -Pointing Commands at a Smart Card Certificate -============================================= - -``rcodesign`` command that operate against certificates expose a -``--smartcard-slot`` argument to specify which smartcard slot to use. - -Slot ``9c`` is the standard slot for holding certificates used for -signing. - -To sign with your smart card certificate at slot ``9c``, do something like:: - - rcodesign sign \ - --smartcard-slot 9c \ - path/to/entity/to/sign - -Smartcards often require a PIN on signing operations. You should be prompted -for your PIN value if the signing operation is initially unauthenticated. - -Importing Certificates Into a Smart Card -======================================== - -The ``rcodesign smartcard-import`` command can be used to import an existing -code signing certificate into your smart card. - -Let's assume you created an Apple code signing certificate and exported it -to the file ``developer_id.p12``. You can import this certificate by doing -the following:: - - $ rcodesign smartcard-import \ - --smartcard-slot 9c \ - --p12-file developer_id.p12 --p12-password password - - $ rcodesign smartcard-scan - Device 0: Yubico YubiKey OTP+FIDO+CCID 0 - Device 0: Serial: 1234567 - Device 0: Version: 5.2.7 - Device 0: Certificate in slot Signature / 9c - Subject CN: Developer ID Application: Gregory Szorc (MK22MZP987) - Issuer CN: Developer ID Certification Authority - Subject is Issuer?: false - Team ID: MK22MZP987 - SHA-1 fingerprint: 44d7155bcabf3b9a9221b01b8e198040ae04e0ad - SHA-256 fingerprint: 8f610de4caea4bc138e85b56726ed4d330f7464d99cfa5957568904b6a6375ec - Signed by Apple?: true - Apple Issuing Chain: - - Developer ID Certification Authority - - Apple Root CA - - Apple Root Certificate Authority - Guessed Certificate Profile: DeveloperIdApplication - Is Apple Root CA?: false - Is Apple Intermediate CA?: false - Apple CA Extension: none - Apple Extended Key Usage Purpose Extensions: - - 1.3.6.1.5.5.7.3.3 (CodeSigning) - Apple Code Signing Extensions: - - 1.2.840.113635.100.6.1.33 (DeveloperIdDate) - - 1.2.840.113635.100.6.1.13 (DeveloperIdApplication) - -Creating a Certificate with a Private Key Exclusive to the Smart Card -===================================================================== - -It is possible to generate a private key directly on the smart card and create -a code signing certificate derived from this private key. - -Code signing certificates created this way are theoretically much more secure -than other private key generation methods because most smart cards never allow the -private key content to be exported/viewed. Assuming operations involving the -private key are protected with the appropriate access protections (like pin or -touch policies), compromise of the machine or even the smart key itself may not -result in unwanted access to the private key. - -To create a code signing certificate whose private key has never left the -smart card device itself, do something like the following. - -First, generate a new private key on the smart card:: - - rcodesign smartcard-generate-key --smartcard-slot 9c - -Then create a certificate signing request (CSR):: - - rcodesign generate-certificate-signing-request \ - --smartcard-slot 9c \ - --csr-pem-path csr.pem - -Then follow the instructions at :ref:`apple_codesign_exchange_csr` to submit the -CSR file to Apple and obtain a *public certificate*. - -Finally, import the Apple-issued public certificate into the smart card:: - - rcodesign smartcard-import \ - --der-source developerID_application.cer \ - --smartcard-slot 9c - -At this point, the smart card is ready to sign using an Apple issued certificate -and the private key never has - and probably never will - leave the smart card -itself. diff --git a/apple-codesign/docs/conf.py b/apple-codesign/docs/conf.py deleted file mode 100644 index 41bf2c44d..000000000 --- a/apple-codesign/docs/conf.py +++ /dev/null @@ -1,34 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import os -import pathlib -import re - -HERE = pathlib.Path(os.path.dirname(__file__)) -ROOT = pathlib.Path(os.path.dirname(HERE)) - -release = "unknown" - -with (ROOT / "Cargo.toml").open("r") as fh: - for line in fh: - m = re.match('^version = "([^"]+)"', line) - if m: - release = m.group(1) - break - - -project = "Apple Codesign" -copyright = "2022, Gregory Szorc" -author = "Gregory Szorc" -extensions = ["sphinx.ext.intersphinx"] -templates_path = ["_templates"] -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -html_theme = "alabaster" -master_doc = "index" -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), - "setuptools": ("https://setuptools.pypa.io/en/latest", None), -} -tags.add("apple_codesign") diff --git a/apple-codesign/docs/index.rst b/apple-codesign/docs/index.rst deleted file mode 100644 index 4e4e75565..000000000 --- a/apple-codesign/docs/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -================== -Apple Code Signing -================== - -.. toctree:: - :maxdepth: 2 - - apple_codesign diff --git a/apple-codesign/src/app_store_connect/api_token.rs b/apple-codesign/src/app_store_connect/api_token.rs deleted file mode 100644 index 61114d721..000000000 --- a/apple-codesign/src/app_store_connect/api_token.rs +++ /dev/null @@ -1,153 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! App Store Connect API tokens. - -use { - crate::AppleCodesignError, - jsonwebtoken::{Algorithm, EncodingKey, Header}, - serde::{Deserialize, Serialize}, - std::{path::Path, time::SystemTime}, -}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -struct ConnectTokenRequest { - iss: String, - iat: u64, - exp: u64, - aud: String, -} - -/// A JWT Token for use with App Store Connect API. -pub type AppStoreConnectToken = String; - -/// Represents a private key used to create JWT tokens for use with App Store Connect. -/// -/// See https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api -/// and https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests -/// for more details. -/// -/// This entity holds the necessary metadata to issue new JWT tokens. -/// -/// App Store Connect API tokens/JWTs are derived from: -/// -/// * A key identifier. This is a short alphanumeric string like `DEADBEEF42`. -/// * An issuer ID. This is likely a UUID. -/// * A private key. Likely ECDSA. -/// -/// All these are issued by Apple. You can log in to App Store Connect and see/manage your keys -/// at https://appstoreconnect.apple.com/access/api. -#[derive(Clone)] -pub struct ConnectTokenEncoder { - key_id: String, - issuer_id: String, - encoding_key: EncodingKey, -} - -impl ConnectTokenEncoder { - /// Construct an instance from an [EncodingKey] instance. - /// - /// This is the lowest level API and ultimately what all constructors use. - pub fn from_jwt_encoding_key( - key_id: String, - issuer_id: String, - encoding_key: EncodingKey, - ) -> Self { - Self { - key_id, - issuer_id, - encoding_key, - } - } - - /// Construct an instance from a DER encoded ECDSA private key. - pub fn from_ecdsa_der( - key_id: String, - issuer_id: String, - der_data: &[u8], - ) -> Result { - let encoding_key = EncodingKey::from_ec_der(der_data); - - Ok(Self::from_jwt_encoding_key(key_id, issuer_id, encoding_key)) - } - - /// Create a token from a PEM encoded ECDSA private key. - pub fn from_ecdsa_pem( - key_id: String, - issuer_id: String, - pem_data: &[u8], - ) -> Result { - let encoding_key = EncodingKey::from_ec_pem(pem_data)?; - - Ok(Self::from_jwt_encoding_key(key_id, issuer_id, encoding_key)) - } - - /// Create a token from a PEM encoded ECDSA private key in a filesystem path. - pub fn from_ecdsa_pem_path( - key_id: String, - issuer_id: String, - path: impl AsRef, - ) -> Result { - let data = std::fs::read(path.as_ref())?; - - Self::from_ecdsa_pem(key_id, issuer_id, &data) - } - - /// Attempt to construct in instance from an API Key ID. - /// - /// e.g. `DEADBEEF42`. This looks for an `AuthKey_.p8` file in default search - /// locations like `~/.appstoreconnect/private_keys`. - pub fn from_api_key_id(key_id: String, issuer_id: String) -> Result { - let mut search_paths = vec![std::env::current_dir()?.join("private_keys")]; - - if let Some(home) = dirs::home_dir() { - search_paths.extend([ - home.join("private_keys"), - home.join(".private_keys"), - home.join(".appstoreconnect").join("private_keys"), - ]); - } - - // AuthKey_.p8 - let filename = format!("AuthKey_{}.p8", key_id); - - for path in search_paths { - let candidate = path.join(&filename); - - if candidate.exists() { - return Self::from_ecdsa_pem_path(key_id, issuer_id, candidate); - } - } - - Err(AppleCodesignError::AppStoreConnectApiKeyNotFound) - } - - /// Mint a new JWT token. - /// - /// Using the private key and key metadata bound to this instance, we issue a new JWT - /// for the requested duration. - pub fn new_token(&self, duration: u64) -> Result { - let header = Header { - kid: Some(self.key_id.clone()), - alg: Algorithm::ES256, - ..Default::default() - }; - - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("calculating UNIX time should never fail") - .as_secs(); - - let claims = ConnectTokenRequest { - iss: self.issuer_id.clone(), - iat: now, - exp: now + duration, - aud: "appstoreconnect-v1".to_string(), - }; - - let token = jsonwebtoken::encode(&header, &claims, &self.encoding_key)?; - - Ok(token) - } -} diff --git a/apple-codesign/src/app_store_connect/mod.rs b/apple-codesign/src/app_store_connect/mod.rs deleted file mode 100644 index 06920ad82..000000000 --- a/apple-codesign/src/app_store_connect/mod.rs +++ /dev/null @@ -1,200 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -pub mod api_token; -pub mod notary_api; - -use { - self::api_token::{AppStoreConnectToken, ConnectTokenEncoder}, - crate::AppleCodesignError, - log::{debug, error}, - reqwest::blocking::Client, - serde::{de::DeserializeOwned, Deserialize, Serialize}, - serde_json::Value, - std::{fs::Permissions, io::Write, path::Path, sync::Mutex}, -}; - -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; - -#[cfg(unix)] -fn set_permissions_private(p: &mut Permissions) { - p.set_mode(0o600); -} - -#[cfg(windows)] -fn set_permissions_private(_: &mut Permissions) {} - -/// Represents all metadata for an App Store Connect API Key. -/// -/// This is a convenience type to aid in the generic representation of all the components -/// of an App Store Connect API Key. The type supports serialization so we save as a single -/// file or payload to enhance usability (so people don't need to provide all 3 pieces of the -/// API Key for all operations). -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct UnifiedApiKey { - /// Who issued the key. - /// - /// Likely a UUID. - issuer_id: String, - - /// Key identifier. - /// - /// An alphanumeric string like `DEADBEEF42`. - key_id: String, - - /// Base64 encoded DER of ECDSA private key material. - private_key: String, -} - -impl UnifiedApiKey { - /// Construct an instance from constitute parts and a PEM encoded ECDSA private key. - /// - /// This is what you want to use if importing a private key from the file downloaded - /// from the App Store Connect web interface. - pub fn from_ecdsa_pem_path( - issuer_id: impl ToString, - key_id: impl ToString, - path: impl AsRef, - ) -> Result { - let pem_data = std::fs::read(path.as_ref())?; - - let parsed = pem::parse(pem_data).map_err(|e| { - AppleCodesignError::AppStoreConnectApiKey(format!("error parsing PEM: {}", e)) - })?; - - if parsed.tag != "PRIVATE KEY" { - return Err(AppleCodesignError::AppStoreConnectApiKey( - "does not look like a PRIVATE KEY".to_string(), - )); - } - - let private_key = base64::encode(parsed.contents); - - Ok(Self { - issuer_id: issuer_id.to_string(), - key_id: key_id.to_string(), - private_key, - }) - } - - /// Construct an instance from serialized JSON. - pub fn from_json(data: impl AsRef<[u8]>) -> Result { - Ok(serde_json::from_slice(data.as_ref())?) - } - - /// Construct an instance from a JSON file. - pub fn from_json_path(path: impl AsRef) -> Result { - let data = std::fs::read(path.as_ref())?; - - Self::from_json(data) - } - - /// Serialize this instance to a JSON object. - pub fn to_json_string(&self) -> Result { - Ok(serde_json::to_string_pretty(&self)?) - } - - /// Write this instance to a JSON file. - /// - /// Since the file contains sensitive data, it will have limited read permissions - /// on platforms where this is implemented. Parent directories will be created if missing - /// using default permissions for created directories. - /// - /// Permissions on the resulting file may not be as restrictive as desired. It is up - /// to callers to additionally harden as desired. - pub fn write_json_file(&self, path: impl AsRef) -> Result<(), AppleCodesignError> { - let path = path.as_ref(); - - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - - let data = self.to_json_string()?; - - let mut fh = std::fs::File::create(path)?; - let mut permissions = fh.metadata()?.permissions(); - set_permissions_private(&mut permissions); - fh.set_permissions(permissions)?; - fh.write_all(data.as_bytes())?; - - Ok(()) - } -} - -impl TryFrom for ConnectTokenEncoder { - type Error = AppleCodesignError; - - fn try_from(value: UnifiedApiKey) -> Result { - let der = base64::decode(value.private_key).map_err(|e| { - AppleCodesignError::AppStoreConnectApiKey(format!( - "failed to base64 decode private key: {}", - e - )) - })?; - - Self::from_ecdsa_der(value.key_id, value.issuer_id, &der) - } -} - -/// A client for App Store Connect API. -/// -/// The client isn't generic. Don't get any ideas. -pub struct AppStoreConnectClient { - client: Client, - connect_token: ConnectTokenEncoder, - token: Mutex>, -} - -impl AppStoreConnectClient { - /// Create a new client to the App Store Connect API. - pub fn new(connect_token: ConnectTokenEncoder) -> Result { - Ok(Self { - client: crate::ticket_lookup::default_client()?, - connect_token, - token: Mutex::new(None), - }) - } - - fn get_token(&self) -> Result { - let mut token = self.token.lock().unwrap(); - - // TODO need to handle token expiration. - if token.is_none() { - token.replace(self.connect_token.new_token(300)?); - } - - Ok(token.as_ref().unwrap().clone()) - } - - pub(crate) fn send_request( - &self, - request: reqwest::blocking::RequestBuilder, - ) -> Result { - let request = request.build()?; - let url = request.url().to_string(); - - debug!("{} {}", request.method(), url); - - let response = self.client.execute(request)?; - - if response.status().is_success() { - Ok(response.json::()?) - } else { - error!("HTTP error from {}", url); - - let body = response.bytes()?; - - if let Ok(value) = serde_json::from_slice::(body.as_ref()) { - for line in serde_json::to_string_pretty(&value)?.lines() { - error!("{}", line); - } - } else { - error!("{}", String::from_utf8_lossy(body.as_ref())); - } - - Err(AppleCodesignError::NotarizeServerError) - } - } -} diff --git a/apple-codesign/src/app_store_connect/notary_api.rs b/apple-codesign/src/app_store_connect/notary_api.rs deleted file mode 100644 index 16c5f6e72..000000000 --- a/apple-codesign/src/app_store_connect/notary_api.rs +++ /dev/null @@ -1,226 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! App Store Connect Notary API. -//! -//! See also . - -use { - crate::{app_store_connect::AppStoreConnectClient, AppleCodesignError}, - serde::{Deserialize, Serialize}, - serde_json::Value, - std::ops::Deref, -}; - -pub const APPLE_NOTARY_SUBMIT_SOFTWARE_URL: &str = - "https://appstoreconnect.apple.com/notary/v2/submissions"; - -/// A notification that the notary service sends you when notarization finishes. -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NewSubmissionRequestNotification { - pub channel: String, - pub target: String, -} - -/// Data that you provide when starting a submission to the notary service. -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NewSubmissionRequest { - pub notifications: Vec, - pub sha256: String, - pub submission_name: String, -} - -/// Information that you use to upload your software for notarization. -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NewSubmissionResponseDataAttributes { - pub aws_access_key_id: String, - pub aws_secret_access_key: String, - pub aws_session_token: String, - pub bucket: String, - pub object: String, -} - -/// Information that the notary service provides for uploading your software for notarization and -/// tracking the submission. -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NewSubmissionResponseData { - pub attributes: NewSubmissionResponseDataAttributes, - pub id: String, - pub r#type: String, -} - -/// The notary service’s response to a software submission. -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NewSubmissionResponse { - pub data: NewSubmissionResponseData, - pub meta: Value, -} - -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "PascalCase")] -pub enum SubmissionResponseStatus { - Accepted, - #[serde(rename = "In Progress")] - InProgress, - Invalid, - Rejected, - #[serde(other)] - Unknown, -} - -/// Information about the status of a submission. -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SubmissionResponseDataAttributes { - pub created_date: String, - pub name: String, - pub status: SubmissionResponseStatus, -} - -/// Information that the service provides about the status of a notarization submission. -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SubmissionResponseData { - pub attributes: SubmissionResponseDataAttributes, - pub id: String, - pub r#type: String, -} - -/// The notary service’s response to a request for the status of a submission. -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SubmissionResponse { - pub data: SubmissionResponseData, - pub meta: Value, -} - -impl SubmissionResponse { - /// Convert the instance into a [Result]. - /// - /// Will yield [Err] if the notarization/upload was not successful. - pub fn into_result(self) -> Result { - match self.data.attributes.status { - SubmissionResponseStatus::Accepted => Ok(self), - SubmissionResponseStatus::InProgress => Err(AppleCodesignError::NotarizeIncomplete), - SubmissionResponseStatus::Invalid => Err(AppleCodesignError::NotarizeInvalid), - SubmissionResponseStatus::Rejected => Err(AppleCodesignError::NotarizeRejected( - 0, - "Notarization error".into(), - )), - SubmissionResponseStatus::Unknown => Err(AppleCodesignError::NotarizeInvalid), - } - } -} - -/// Information about the log associated with the submission. -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SubmissionLogResponseDataAttributes { - pub developer_log_url: String, -} - -/// Data that indicates how to get the log information for a particular submission. -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SubmissionLogResponseData { - pub attributes: SubmissionLogResponseDataAttributes, - pub id: String, - pub r#type: String, -} - -/// The notary service’s response to a request for the log information about a completed submission. -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SubmissionLogResponse { - pub data: SubmissionLogResponseData, - pub meta: Value, -} - -/// A client to the App Store Connect Notary API. -pub struct NotaryApiClient(AppStoreConnectClient); - -impl Deref for NotaryApiClient { - type Target = AppStoreConnectClient; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for NotaryApiClient { - fn from(v: AppStoreConnectClient) -> Self { - Self(v) - } -} - -impl NotaryApiClient { - /// Create a submission to the Notary API. - pub fn create_submission( - &self, - sha256: &str, - submission_name: &str, - ) -> Result { - let token = self.get_token()?; - - let body = NewSubmissionRequest { - notifications: Vec::new(), - sha256: sha256.to_string(), - submission_name: submission_name.to_string(), - }; - let req = self - .client - .post(APPLE_NOTARY_SUBMIT_SOFTWARE_URL) - .bearer_auth(token) - .header("Accept", "application/json") - .header("Content-Type", "application/json") - .json(&body); - - self.send_request(req) - } - - /// Fetch the status of a Notary API submission. - pub fn get_submission( - &self, - submission_id: &str, - ) -> Result { - let token = self.get_token()?; - - let req = self - .client - .get(format!( - "{}/{}", - APPLE_NOTARY_SUBMIT_SOFTWARE_URL, submission_id - )) - .bearer_auth(token) - .header("Accept", "application/json"); - - self.send_request(req) - } - - /// Fetch details about a single completed notarization. - pub fn get_submission_log(&self, submission_id: &str) -> Result { - let token = self.get_token()?; - - let req = self - .client - .get(format!( - "{}/{}/logs", - APPLE_NOTARY_SUBMIT_SOFTWARE_URL, submission_id - )) - .bearer_auth(token) - .header("Accept", "application/json"); - - let res = self.send_request::(req)?; - - let url = res.data.attributes.developer_log_url; - let logs = self.client.get(url).send()?.json::()?; - - Ok(logs) - } -} diff --git a/apple-codesign/src/apple-certs/AppleAAI2CA.cer b/apple-codesign/src/apple-certs/AppleAAI2CA.cer deleted file mode 100644 index 0038b6043..000000000 Binary files a/apple-codesign/src/apple-certs/AppleAAI2CA.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleAAICA.cer b/apple-codesign/src/apple-certs/AppleAAICA.cer deleted file mode 100644 index e21d7dfa4..000000000 Binary files a/apple-codesign/src/apple-certs/AppleAAICA.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleAAICAG3.cer b/apple-codesign/src/apple-certs/AppleAAICAG3.cer deleted file mode 100644 index 0f3cdf74e..000000000 Binary files a/apple-codesign/src/apple-certs/AppleAAICAG3.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleApplicationIntegrationCA5G1.cer b/apple-codesign/src/apple-certs/AppleApplicationIntegrationCA5G1.cer deleted file mode 100644 index 201582102..000000000 Binary files a/apple-codesign/src/apple-certs/AppleApplicationIntegrationCA5G1.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleComputerRootCertificate.cer b/apple-codesign/src/apple-certs/AppleComputerRootCertificate.cer deleted file mode 100644 index 8ccb85c5e..000000000 Binary files a/apple-codesign/src/apple-certs/AppleComputerRootCertificate.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleISTCA2G1.cer b/apple-codesign/src/apple-certs/AppleISTCA2G1.cer deleted file mode 100644 index 46711ce49..000000000 Binary files a/apple-codesign/src/apple-certs/AppleISTCA2G1.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleISTCA8G1.cer b/apple-codesign/src/apple-certs/AppleISTCA8G1.cer deleted file mode 100644 index ecddd53dd..000000000 Binary files a/apple-codesign/src/apple-certs/AppleISTCA8G1.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleIncRootCertificate.cer b/apple-codesign/src/apple-certs/AppleIncRootCertificate.cer deleted file mode 100644 index 8a9ff2474..000000000 Binary files a/apple-codesign/src/apple-certs/AppleIncRootCertificate.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleRootCA-G2.cer b/apple-codesign/src/apple-certs/AppleRootCA-G2.cer deleted file mode 100644 index 739b81413..000000000 Binary files a/apple-codesign/src/apple-certs/AppleRootCA-G2.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleRootCA-G3.cer b/apple-codesign/src/apple-certs/AppleRootCA-G3.cer deleted file mode 100644 index 228bfa39c..000000000 Binary files a/apple-codesign/src/apple-certs/AppleRootCA-G3.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleSoftwareUpdateCertificationAuthority.cer b/apple-codesign/src/apple-certs/AppleSoftwareUpdateCertificationAuthority.cer deleted file mode 100644 index 564896b3f..000000000 Binary files a/apple-codesign/src/apple-certs/AppleSoftwareUpdateCertificationAuthority.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleTimestampCA.cer b/apple-codesign/src/apple-certs/AppleTimestampCA.cer deleted file mode 100644 index dc0538f00..000000000 Binary files a/apple-codesign/src/apple-certs/AppleTimestampCA.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleWWDRCA.cer b/apple-codesign/src/apple-certs/AppleWWDRCA.cer deleted file mode 100644 index d2bb1da64..000000000 Binary files a/apple-codesign/src/apple-certs/AppleWWDRCA.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleWWDRCAG2.cer b/apple-codesign/src/apple-certs/AppleWWDRCAG2.cer deleted file mode 100644 index b77e1e9eb..000000000 Binary files a/apple-codesign/src/apple-certs/AppleWWDRCAG2.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleWWDRCAG3.cer b/apple-codesign/src/apple-certs/AppleWWDRCAG3.cer deleted file mode 100644 index 32f96f81d..000000000 Binary files a/apple-codesign/src/apple-certs/AppleWWDRCAG3.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleWWDRCAG4.cer b/apple-codesign/src/apple-certs/AppleWWDRCAG4.cer deleted file mode 100644 index b9f0bf298..000000000 Binary files a/apple-codesign/src/apple-certs/AppleWWDRCAG4.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleWWDRCAG5.cer b/apple-codesign/src/apple-certs/AppleWWDRCAG5.cer deleted file mode 100644 index 8b564c768..000000000 Binary files a/apple-codesign/src/apple-certs/AppleWWDRCAG5.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/AppleWWDRCAG6.cer b/apple-codesign/src/apple-certs/AppleWWDRCAG6.cer deleted file mode 100644 index 424a70bd3..000000000 Binary files a/apple-codesign/src/apple-certs/AppleWWDRCAG6.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/DevAuthCA.cer b/apple-codesign/src/apple-certs/DevAuthCA.cer deleted file mode 100644 index 3d8fb2764..000000000 Binary files a/apple-codesign/src/apple-certs/DevAuthCA.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/DeveloperIDCA.cer b/apple-codesign/src/apple-certs/DeveloperIDCA.cer deleted file mode 100644 index d3337393b..000000000 Binary files a/apple-codesign/src/apple-certs/DeveloperIDCA.cer and /dev/null differ diff --git a/apple-codesign/src/apple-certs/DeveloperIDG2CA.cer b/apple-codesign/src/apple-certs/DeveloperIDG2CA.cer deleted file mode 100644 index 8cbcf6f46..000000000 Binary files a/apple-codesign/src/apple-certs/DeveloperIDG2CA.cer and /dev/null differ diff --git a/apple-codesign/src/apple_certificates.rs b/apple-codesign/src/apple_certificates.rs deleted file mode 100644 index c41e79ce9..000000000 --- a/apple-codesign/src/apple_certificates.rs +++ /dev/null @@ -1,548 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Apple X.509 certificates. -//! -//! This module defines well-known Apple X.509 certificates. -//! -//! The canonical source of this data is . -//! -//! Note that some certificates are commented out and not available -//! because the official DER-encoded certificates provided by Apple -//! do not conform to the encoding standards in RFC 5280. - -use {once_cell::sync::Lazy, std::ops::Deref, x509_certificate::CapturedX509Certificate}; - -/// Apple Inc. Root Certificate -static APPLE_INC_ROOT_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der( - include_bytes!("apple-certs/AppleIncRootCertificate.cer").to_vec(), - ) - .unwrap() -}); - -/// Apple Computer, Inc. Root Certificate. -static APPLE_COMPUTER_INC_ROOT_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der( - include_bytes!("apple-certs/AppleComputerRootCertificate.cer").to_vec(), - ) - .unwrap() -}); - -/// Apple Root CA - G2 Root Certificate -static APPLE_ROOT_CA_G2_ROOT_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleRootCA-G2.cer").to_vec()) - .unwrap() -}); - -/// Apple Root CA - G3 Root Certificate -static APPLE_ROOT_CA_G3_ROOT_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleRootCA-G3.cer").to_vec()) - .unwrap() -}); - -/// Apple IST CA 2 - G1 Certificate -static APPLE_IST_CA_2_G1_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleISTCA2G1.cer").to_vec()) - .unwrap() -}); - -/// Apple IST CA 8 - G1 Certificate -static APPLE_IST_CA_8_G1_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleISTCA8G1.cer").to_vec()) - .unwrap() -}); - -/// Application Integration Certificate -static APPLICATION_INTEGRATION_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleAAICA.cer").to_vec()) - .unwrap() -}); - -/// Application Integration 2 Certificate -static APPLICATION_INTEGRATION_2_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleAAI2CA.cer").to_vec()) - .unwrap() -}); - -/// Application Integration - G3 Certificate -static APPLICATION_INTEGRATION_G3_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleAAICAG3.cer").to_vec()) - .unwrap() -}); - -/// Apple Application Integration CA 5 - G1 Certificate -static APPLE_APPLICATION_INTEGRATION_CA_5_G1_CERTIFICATE: Lazy = - Lazy::new(|| { - CapturedX509Certificate::from_der( - include_bytes!("apple-certs/AppleApplicationIntegrationCA5G1.cer").to_vec(), - ) - .unwrap() - }); - -/// Developer Authentication Certificate -static DEVELOPER_AUTHENTICATION_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/DevAuthCA.cer").to_vec()).unwrap() -}); - -/// Developer ID - G1 (Expiring 02/01/2027 22:12:15 UTC) Certificate -static DEVELOPER_ID_G1_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/DeveloperIDCA.cer").to_vec()) - .unwrap() -}); - -/// Developer ID - G2 (Expiring 09/17/2031 00:00:00 UTC) Certificate -static DEVELOPER_ID_G2_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/DeveloperIDG2CA.cer").to_vec()) - .unwrap() -}); - -/// Software Update Certificate -static SOFTWARE_UPDATE_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der( - include_bytes!("apple-certs/AppleSoftwareUpdateCertificationAuthority.cer").to_vec(), - ) - .unwrap() -}); - -/// Timestamp Certificate -static TIMESTAMP_CERTIFICATE: Lazy = Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleTimestampCA.cer").to_vec()) - .unwrap() -}); - -/// Worldwide Developer Relations - G1 (Expiring 02/07/2023 21:48:47 UTC) Certificate -static WORLD_WIDE_DEVELOPER_RELATIONS_G1_CERTIFICATE: Lazy = - Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleWWDRCA.cer").to_vec()) - .unwrap() - }); - -/// Worldwide Developer Relations - G2 (Expiring 05/06/2029 23:43:24 UTC) Certificate -static WORLD_WIDE_DEVELOPER_RELATIONS_G2_CERTIFICATE: Lazy = - Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleWWDRCAG2.cer").to_vec()) - .unwrap() - }); - -/// Worldwide Developer Relations - G3 (Expiring 02/20/2030 00:00:00 UTC) Certificate -static WORLD_WIDE_DEVELOPER_RELATIONS_G3_CERTIFICATE: Lazy = - Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleWWDRCAG3.cer").to_vec()) - .unwrap() - }); - -/// Worldwide Developer Relations - G4 (Expiring 12/10/2030 00:00:00 UTC) Certificate -static WORLD_WIDE_DEVELOPER_RELATIONS_G4_CERTIFICATE: Lazy = - Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleWWDRCAG4.cer").to_vec()) - .unwrap() - }); - -/// Worldwide Developer Relations - G5 (Expiring 12/10/2030 00:00:00 UTC) Certificate -static WORLD_WIDE_DEVELOPER_RELATIONS_G5_CERTIFICATE: Lazy = - Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleWWDRCAG5.cer").to_vec()) - .unwrap() - }); - -/// Worldwide Developer Relations - G6 (Expiring 03/19/2036 00:00:00 UTC) Certificate -static WORLD_WIDE_DEVELOPER_RELATIONS_G6_CERTIFICATE: Lazy = - Lazy::new(|| { - CapturedX509Certificate::from_der(include_bytes!("apple-certs/AppleWWDRCAG6.cer").to_vec()) - .unwrap() - }); - -/// All known Apple certificates. -static KNOWN_CERTIFICATES: Lazy> = Lazy::new(|| { - vec![ - // We put the 4 roots first, newest to oldest. - APPLE_ROOT_CA_G3_ROOT_CERTIFICATE.deref(), - APPLE_ROOT_CA_G2_ROOT_CERTIFICATE.deref(), - APPLE_INC_ROOT_CERTIFICATE.deref(), - APPLE_COMPUTER_INC_ROOT_CERTIFICATE.deref(), - APPLE_IST_CA_2_G1_CERTIFICATE.deref(), - APPLE_IST_CA_8_G1_CERTIFICATE.deref(), - APPLICATION_INTEGRATION_CERTIFICATE.deref(), - APPLICATION_INTEGRATION_2_CERTIFICATE.deref(), - APPLICATION_INTEGRATION_G3_CERTIFICATE.deref(), - APPLE_APPLICATION_INTEGRATION_CA_5_G1_CERTIFICATE.deref(), - DEVELOPER_AUTHENTICATION_CERTIFICATE.deref(), - DEVELOPER_ID_G1_CERTIFICATE.deref(), - DEVELOPER_ID_G2_CERTIFICATE.deref(), - SOFTWARE_UPDATE_CERTIFICATE.deref(), - TIMESTAMP_CERTIFICATE.deref(), - WORLD_WIDE_DEVELOPER_RELATIONS_G1_CERTIFICATE.deref(), - WORLD_WIDE_DEVELOPER_RELATIONS_G2_CERTIFICATE.deref(), - WORLD_WIDE_DEVELOPER_RELATIONS_G3_CERTIFICATE.deref(), - WORLD_WIDE_DEVELOPER_RELATIONS_G4_CERTIFICATE.deref(), - WORLD_WIDE_DEVELOPER_RELATIONS_G5_CERTIFICATE.deref(), - WORLD_WIDE_DEVELOPER_RELATIONS_G6_CERTIFICATE.deref(), - ] -}); - -static KNOWN_ROOTS: Lazy> = Lazy::new(|| { - vec![ - APPLE_ROOT_CA_G3_ROOT_CERTIFICATE.deref(), - APPLE_ROOT_CA_G2_ROOT_CERTIFICATE.deref(), - APPLE_INC_ROOT_CERTIFICATE.deref(), - APPLE_COMPUTER_INC_ROOT_CERTIFICATE.deref(), - ] -}); - -/// Defines all known Apple certificates. -/// -/// This crate embeds the raw certificate data for the various known -/// Apple certificate authorities, as advertised at -/// . -/// -/// This enumeration defines all the ones we know about. Instances can -/// be dereferenced into concrete [CapturedX509Certificate] to get at the underlying -/// certificate and access its metadata. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum KnownCertificate { - /// Apple Computer, Inc. Root Certificate. - /// - /// C = US, O = "Apple Computer, Inc.", OU = Apple Computer Certificate Authority, CN = Apple Root Certificate Authority - AppleComputerIncRoot, - - /// Apple Inc. Root Certificate - /// - /// C = US, O = Apple Inc., OU = Apple Certification Authority, CN = Apple Root CA - AppleRootCa, - - /// Apple Root CA - G2 Root Certificate - /// - /// CN = Apple Root CA - G2, OU = Apple Certification Authority, O = Apple Inc., C = US - AppleRootCaG2Root, - - /// Apple Root CA - G3 Root Certificate - /// - /// CN = Apple Root CA - G3, OU = Apple Certification Authority, O = Apple Inc., C = US - AppleRootCaG3Root, - - /// Apple IST CA 2 - G1 Certificate - /// - /// CN = Apple IST CA 2 - G1, OU = Certification Authority, O = Apple Inc., C = US - AppleIstCa2G1, - - /// Apple IST CA 8 - G1 Certificate - /// - /// CN = Apple IST CA 8 - G1, OU = Certification Authority, O = Apple Inc., C = US - AppleIstCa8G1, - - /// Application Integration Certificate - /// - /// C = US, O = Apple Inc., OU = Apple Certification Authority, CN = Apple Application Integration Certification Authority - ApplicationIntegration, - - /// Application Integration 2 Certificate - /// - /// CN = Apple Application Integration 2 Certification Authority, OU = Apple Certification Authority, O = Apple Inc., C = US - ApplicationIntegration2, - - /// Application Integration - G3 Certificate - /// - /// CN = Apple Application Integration CA - G3, OU = Apple Certification Authority, O = Apple Inc., C = US - ApplicationIntegrationG3, - - /// Apple Application Integration CA 5 - G1 Certificate - /// - /// CN = Apple Application Integration CA 5 - G1, OU = Apple Certification Authority, O = Apple Inc., C = US - AppleApplicationIntegrationCa5G1, - - /// Developer Authentication Certificate - /// - /// CN = Developer Authentication Certification Authority, OU = Apple Worldwide Developer Relations, O = Apple Inc., C = US - DeveloperAuthentication, - - /// Developer ID - G1 Certificate - /// - /// CN = Developer ID Certification Authority, OU = Apple Certification Authority, O = Apple Inc., C = US - DeveloperIdG1, - - /// Developer ID - G2 Certificate. - /// - /// CN = Developer ID Certification Authority, OU = G2, O = Apple Inc., C = US - DeveloperIdG2, - - /// Software Update Certificate - /// - /// CN = Apple Software Update Certification Authority, OU = Certification Authority, O = Apple Inc., C = US - SoftwareUpdate, - - /// Timestamp Certificate - /// - /// CN = Apple Timestamp Certification Authority, OU = Apple Certification Authority, O = Apple Inc., C = US - Timestamp, - - /// Worldwide Developer Relations - G1 (Expiring 02/07/2023 21:48:47 UTC) Certificate - /// - /// C = US, O = Apple Inc., OU = Apple Worldwide Developer Relations, CN = Apple Worldwide Developer Relations Certification Authority - WwdrG1, - - /// Worldwide Developer Relations - G2 (Expiring 05/06/2029 23:43:24 UTC) Certificate - /// - /// CN = Apple Worldwide Developer Relations CA - G2, OU = Apple Certification Authority, O = Apple Inc., C = US - WwdrG2, - - /// Worldwide Developer Relations - G3 (Expiring 02/20/2030 00:00:00 UTC) Certificate - /// - /// CN = Apple Worldwide Developer Relations Certification Authority, OU = G3, O = Apple Inc., C = US - WwdrG3, - - /// Worldwide Developer Relations - G4 (Expiring 12/10/2030 00:00:00 UTC) Certificate - /// - /// CN = Apple Worldwide Developer Relations Certification Authority, OU = G4, O = Apple Inc., C = US - WwdrG4, - - /// Worldwide Developer Relations - G5 (Expiring 12/10/2030 00:00:00 UTC) Certificate - /// - /// CN = Apple Worldwide Developer Relations Certification Authority, OU = G5, O = Apple Inc., C = US - WwdrG5, - - /// Worldwide Developer Relations - G6 (Expiring 03/19/2036 00:00:00 UTC) Certificate - /// - /// CN = Apple Worldwide Developer Relations Certification Authority, OU = G6, O = Apple Inc., C = US - WwdrG6, -} - -impl Deref for KnownCertificate { - type Target = CapturedX509Certificate; - - fn deref(&self) -> &Self::Target { - match self { - Self::AppleComputerIncRoot => APPLE_COMPUTER_INC_ROOT_CERTIFICATE.deref(), - Self::AppleRootCa => APPLE_INC_ROOT_CERTIFICATE.deref(), - Self::AppleRootCaG2Root => APPLE_ROOT_CA_G2_ROOT_CERTIFICATE.deref(), - Self::AppleRootCaG3Root => APPLE_ROOT_CA_G3_ROOT_CERTIFICATE.deref(), - Self::AppleIstCa2G1 => APPLE_IST_CA_2_G1_CERTIFICATE.deref(), - Self::AppleIstCa8G1 => APPLE_IST_CA_8_G1_CERTIFICATE.deref(), - Self::ApplicationIntegration => APPLICATION_INTEGRATION_CERTIFICATE.deref(), - Self::ApplicationIntegration2 => APPLICATION_INTEGRATION_2_CERTIFICATE.deref(), - Self::ApplicationIntegrationG3 => APPLICATION_INTEGRATION_G3_CERTIFICATE.deref(), - Self::AppleApplicationIntegrationCa5G1 => { - APPLE_APPLICATION_INTEGRATION_CA_5_G1_CERTIFICATE.deref() - } - Self::DeveloperAuthentication => DEVELOPER_AUTHENTICATION_CERTIFICATE.deref(), - Self::DeveloperIdG1 => DEVELOPER_ID_G1_CERTIFICATE.deref(), - Self::DeveloperIdG2 => DEVELOPER_ID_G2_CERTIFICATE.deref(), - Self::SoftwareUpdate => SOFTWARE_UPDATE_CERTIFICATE.deref(), - Self::Timestamp => TIMESTAMP_CERTIFICATE.deref(), - Self::WwdrG1 => WORLD_WIDE_DEVELOPER_RELATIONS_G1_CERTIFICATE.deref(), - Self::WwdrG2 => WORLD_WIDE_DEVELOPER_RELATIONS_G2_CERTIFICATE.deref(), - Self::WwdrG3 => WORLD_WIDE_DEVELOPER_RELATIONS_G3_CERTIFICATE.deref(), - Self::WwdrG4 => WORLD_WIDE_DEVELOPER_RELATIONS_G4_CERTIFICATE.deref(), - Self::WwdrG5 => WORLD_WIDE_DEVELOPER_RELATIONS_G5_CERTIFICATE.deref(), - Self::WwdrG6 => WORLD_WIDE_DEVELOPER_RELATIONS_G6_CERTIFICATE.deref(), - } - } -} - -impl AsRef for KnownCertificate { - fn as_ref(&self) -> &CapturedX509Certificate { - self.deref() - } -} - -impl TryFrom<&CapturedX509Certificate> for KnownCertificate { - type Error = &'static str; - - fn try_from(cert: &CapturedX509Certificate) -> Result { - let want = cert.constructed_data(); - - match cert.constructed_data() { - _ if APPLE_ROOT_CA_G3_ROOT_CERTIFICATE.constructed_data() == want => { - Ok(Self::AppleRootCaG3Root) - } - _ if APPLE_ROOT_CA_G2_ROOT_CERTIFICATE.constructed_data() == want => { - Ok(Self::AppleRootCaG2Root) - } - _ if APPLE_INC_ROOT_CERTIFICATE.constructed_data() == want => Ok(Self::AppleRootCa), - _ if APPLE_COMPUTER_INC_ROOT_CERTIFICATE.constructed_data() == want => { - Ok(Self::AppleComputerIncRoot) - } - _ if APPLE_IST_CA_2_G1_CERTIFICATE.constructed_data() == want => { - Ok(Self::AppleIstCa2G1) - } - _ if APPLE_IST_CA_8_G1_CERTIFICATE.constructed_data() == want => { - Ok(Self::AppleIstCa8G1) - } - _ if APPLICATION_INTEGRATION_CERTIFICATE.constructed_data() == want => { - Ok(Self::ApplicationIntegration) - } - _ if APPLICATION_INTEGRATION_2_CERTIFICATE.constructed_data() == want => { - Ok(Self::ApplicationIntegration2) - } - _ if APPLICATION_INTEGRATION_G3_CERTIFICATE.constructed_data() == want => { - Ok(Self::ApplicationIntegrationG3) - } - _ if APPLE_APPLICATION_INTEGRATION_CA_5_G1_CERTIFICATE.constructed_data() == want => { - Ok(Self::AppleApplicationIntegrationCa5G1) - } - _ if DEVELOPER_AUTHENTICATION_CERTIFICATE.constructed_data() == want => { - Ok(Self::DeveloperAuthentication) - } - _ if DEVELOPER_ID_G1_CERTIFICATE.constructed_data() == want => Ok(Self::DeveloperIdG1), - _ if DEVELOPER_ID_G2_CERTIFICATE.constructed_data() == want => Ok(Self::DeveloperIdG2), - _ if SOFTWARE_UPDATE_CERTIFICATE.constructed_data() == want => Ok(Self::SoftwareUpdate), - _ if TIMESTAMP_CERTIFICATE.constructed_data() == want => Ok(Self::Timestamp), - _ if WORLD_WIDE_DEVELOPER_RELATIONS_G1_CERTIFICATE.constructed_data() == want => { - Ok(Self::WwdrG1) - } - _ if WORLD_WIDE_DEVELOPER_RELATIONS_G2_CERTIFICATE.constructed_data() == want => { - Ok(Self::WwdrG2) - } - _ if WORLD_WIDE_DEVELOPER_RELATIONS_G3_CERTIFICATE.constructed_data() == want => { - Ok(Self::WwdrG3) - } - _ if WORLD_WIDE_DEVELOPER_RELATIONS_G4_CERTIFICATE.constructed_data() == want => { - Ok(Self::WwdrG4) - } - _ if WORLD_WIDE_DEVELOPER_RELATIONS_G5_CERTIFICATE.constructed_data() == want => { - Ok(Self::WwdrG5) - } - _ if WORLD_WIDE_DEVELOPER_RELATIONS_G6_CERTIFICATE.constructed_data() == want => { - Ok(Self::WwdrG6) - } - _ => Err("certificate not found"), - } - } -} - -impl KnownCertificate { - /// Obtain a slice of all known [KnownCertificate]. - /// - /// If you want to iterate over all certificates and find one, you can use - /// this. - pub fn all() -> &'static [&'static CapturedX509Certificate] { - KNOWN_CERTIFICATES.deref().as_ref() - } - - /// All of Apple's known root certificate authority certificates. - pub fn all_roots() -> &'static [&'static CapturedX509Certificate] { - KNOWN_ROOTS.deref() - } -} - -#[cfg(test)] -mod test { - use { - super::*, - crate::certificate::{AppleCertificate, CertificateAuthorityExtension}, - }; - - #[test] - fn all() { - for cert in KnownCertificate::all() { - assert!(cert.subject_common_name().is_some()); - assert!(KnownCertificate::try_from(*cert).is_ok()); - } - } - - #[test] - fn apple_root_ca() { - assert!(APPLE_INC_ROOT_CERTIFICATE.is_apple_root_ca()); - assert!(!APPLE_INC_ROOT_CERTIFICATE.is_apple_intermediate_ca()); - assert!(APPLE_COMPUTER_INC_ROOT_CERTIFICATE.is_apple_root_ca()); - assert!(!APPLE_COMPUTER_INC_ROOT_CERTIFICATE.is_apple_intermediate_ca()); - assert!(APPLE_ROOT_CA_G2_ROOT_CERTIFICATE.is_apple_root_ca()); - assert!(!APPLE_ROOT_CA_G2_ROOT_CERTIFICATE.is_apple_intermediate_ca()); - assert!(APPLE_ROOT_CA_G3_ROOT_CERTIFICATE.is_apple_root_ca()); - assert!(!APPLE_ROOT_CA_G3_ROOT_CERTIFICATE.is_apple_intermediate_ca()); - - assert!(!WORLD_WIDE_DEVELOPER_RELATIONS_G3_CERTIFICATE.is_apple_root_ca()); - assert!(WORLD_WIDE_DEVELOPER_RELATIONS_G3_CERTIFICATE.is_apple_intermediate_ca()); - - let wanted = vec![ - APPLE_INC_ROOT_CERTIFICATE.deref(), - APPLE_COMPUTER_INC_ROOT_CERTIFICATE.deref(), - APPLE_ROOT_CA_G2_ROOT_CERTIFICATE.deref(), - APPLE_ROOT_CA_G3_ROOT_CERTIFICATE.deref(), - ]; - - for cert in KnownCertificate::all() { - if wanted.contains(cert) { - continue; - } - - assert!(!cert.is_apple_root_ca()); - assert!(cert.is_apple_intermediate_ca()); - } - } - - #[test] - fn intermediate_have_apple_ca_extension() { - // All intermediate certs should have OIDs identifying them as such. - for cert in KnownCertificate::all() - .iter() - .filter(|cert| !cert.is_apple_root_ca()) - // There are some intermediate certificates signed by GeoTrust. Filter them out - // as well. - .filter(|cert| { - cert.issuer_name() - .iter_common_name() - .all(|atv| !atv.to_string().unwrap().contains("GeoTrust")) - }) - { - assert!(cert.apple_ca_extension().is_some()); - } - - // Let's spot check a few. - assert_eq!( - KnownCertificate::DeveloperIdG1.apple_ca_extension(), - Some(CertificateAuthorityExtension::DeveloperId) - ); - assert_eq!( - KnownCertificate::DeveloperIdG2.apple_ca_extension(), - Some(CertificateAuthorityExtension::DeveloperId) - ); - assert_eq!( - KnownCertificate::WwdrG1.apple_ca_extension(), - Some(CertificateAuthorityExtension::AppleWorldwideDeveloperRelations) - ); - assert_eq!( - KnownCertificate::WwdrG2.apple_ca_extension(), - Some(CertificateAuthorityExtension::AppleWorldwideDeveloperRelationsG2) - ); - assert_eq!( - KnownCertificate::WwdrG3.apple_ca_extension(), - Some(CertificateAuthorityExtension::AppleWorldwideDeveloperRelations) - ); - assert_eq!( - KnownCertificate::WwdrG4.apple_ca_extension(), - Some(CertificateAuthorityExtension::AppleWorldwideDeveloperRelations) - ); - assert_eq!( - KnownCertificate::WwdrG5.apple_ca_extension(), - Some(CertificateAuthorityExtension::AppleWorldwideDeveloperRelations) - ); - assert_eq!( - KnownCertificate::WwdrG6.apple_ca_extension(), - Some(CertificateAuthorityExtension::AppleWorldwideDeveloperRelations) - ); - } - - #[test] - fn chaining() { - let relevant = KnownCertificate::all() - .iter() - .filter(|cert| { - cert.issuer_name() - .iter_common_name() - .all(|atv| !atv.to_string().unwrap().contains("GeoTrust")) - }) - .filter(|cert| { - cert.constructed_data() != APPLICATION_INTEGRATION_G3_CERTIFICATE.constructed_data() - && cert.constructed_data() - != APPLE_APPLICATION_INTEGRATION_CA_5_G1_CERTIFICATE.constructed_data() - }); - - for cert in relevant { - let chain = cert.resolve_signing_chain(KnownCertificate::all().iter().copied()); - let apple_chain = cert.apple_issuing_chain(); - assert_eq!(chain.len(), apple_chain.len()); - } - } -} diff --git a/apple-codesign/src/bundle_signing.rs b/apple-codesign/src/bundle_signing.rs deleted file mode 100644 index dbb296f42..000000000 --- a/apple-codesign/src/bundle_signing.rs +++ /dev/null @@ -1,613 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Functionality for signing Apple bundles. - -use { - crate::{ - code_directory::CodeDirectoryBlob, - code_requirement::RequirementType, - code_resources::{CodeResourcesBuilder, CodeResourcesRule}, - embedded_signature::{Blob, BlobData, DigestType}, - error::AppleCodesignError, - macho::MachFile, - macho_signing::{write_macho_file, MachOSigner}, - signing_settings::{SettingsScope, SigningSettings}, - }, - apple_bundles::{BundlePackageType, DirectoryBundle, DirectoryBundleFile}, - log::{info, warn}, - simple_file_manifest::create_symlink, - std::{ - collections::BTreeMap, - io::Write, - path::{Path, PathBuf}, - }, -}; - -/// Copy a bundle's contents to a destination directory. -pub fn copy_bundle(bundle: &DirectoryBundle, dest_dir: &Path) -> Result<(), AppleCodesignError> { - let settings = SigningSettings::default(); - - let handler = SingleBundleHandler { - dest_dir: dest_dir.to_path_buf(), - settings: &settings, - }; - - for file in bundle - .files(false) - .map_err(AppleCodesignError::DirectoryBundle)? - { - handler.install_file(&file)?; - } - - Ok(()) -} - -/// A primitive for signing an Apple bundle. -/// -/// This type handles the high-level logic of signing an Apple bundle (e.g. -/// a `.app` or `.framework` directory with a well-defined structure). -/// -/// This type handles the signing of nested bundles (if present) such that -/// they chain to the main bundle's signature. -pub struct BundleSigner { - /// All the bundles being signed, indexed by relative path. - bundles: BTreeMap, SingleBundleSigner>, -} - -impl BundleSigner { - /// Construct a new instance given the path to an on-disk bundle. - /// - /// The path should be the root directory of the bundle. e.g. `MyApp.app`. - pub fn new_from_path(path: impl AsRef) -> Result { - let main_bundle = DirectoryBundle::new_from_path(path.as_ref()) - .map_err(AppleCodesignError::DirectoryBundle)?; - - let mut bundles = main_bundle - .nested_bundles(true) - .map_err(AppleCodesignError::DirectoryBundle)? - .into_iter() - .map(|(k, bundle)| (Some(k), SingleBundleSigner::new(bundle))) - .collect::, SingleBundleSigner>>(); - - bundles.insert(None, SingleBundleSigner::new(main_bundle)); - - Ok(Self { bundles }) - } - - /// Write a signed bundle to the given destination directory. - /// - /// The destination directory can be the same as the source directory. However, - /// if this is done and an error occurs in the middle of signing, the bundle - /// may be left in an inconsistent or corrupted state and may not be usable. - pub fn write_signed_bundle( - &self, - dest_dir: impl AsRef, - settings: &SigningSettings, - ) -> Result { - let dest_dir = dest_dir.as_ref(); - - // We need to sign the leaf-most bundles first since a parent bundle may need - // to record information about the child in its signature. - let mut bundles = self - .bundles - .iter() - .filter_map(|(rel, bundle)| rel.as_ref().map(|rel| (rel, bundle))) - .collect::>(); - - // This won't preserve alphabetical order. But since the input was stable, output - // should be deterministic. - bundles.sort_by(|(a, _), (b, _)| b.len().cmp(&a.len())); - - warn!( - "signing {} nested bundles in the following order:", - bundles.len() - ); - for bundle in &bundles { - warn!("{}", bundle.0); - } - - for (rel, nested) in bundles { - let nested_dest_dir = dest_dir.join(rel); - info!( - "entering nested bundle {}", - nested.bundle.root_dir().display(), - ); - - // If we excluded this bundle from signing, just copy all the files. - if settings - .path_exclusion_patterns() - .iter() - .any(|pattern| pattern.matches(rel)) - { - warn!("bundle is in exclusion list; it will be copied instead of signed"); - copy_bundle(&nested.bundle, &nested_dest_dir)?; - } else { - nested.write_signed_bundle( - nested_dest_dir, - &settings.as_nested_bundle_settings(rel), - )?; - } - - info!( - "leaving nested bundle {}", - nested.bundle.root_dir().display() - ); - } - - let main = self - .bundles - .get(&None) - .expect("main bundle should have a key"); - - main.write_signed_bundle(dest_dir, settings) - } -} - -/// Metadata about a signed Mach-O file or bundle. -/// -/// If referring to a bundle, the metadata refers to the 1st Mach-O in the -/// bundle's main executable. -/// -/// This contains enough metadata to construct references to the file/bundle -/// in [crate::code_resources::CodeResources] files. -pub struct SignedMachOInfo { - /// Raw data constituting the code directory blob. - /// - /// Is typically digested to construct a . - pub code_directory_blob: Vec, - - /// Designated code requirements string. - /// - /// Typically occupies a `requirement` in a - /// [crate::code_resources::CodeResources] file. - pub designated_code_requirement: Option, -} - -impl SignedMachOInfo { - /// Parse Mach-O data to obtain an instance. - pub fn parse_data(data: &[u8]) -> Result { - // Initial Mach-O's signature data is used. - let mach = MachFile::parse(data)?; - let macho = mach.nth_macho(0)?; - - let signature = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - // Usually this type is used to chain content digests in the context of bundle signing / - // code resources files. In that context, SHA-256 digests are preferred and might even - // be the only supported digests. So, prefer a SHA-256 code directory over SHA-1. - let cd = if let Some(cd) = signature.code_directory_for_digest(DigestType::Sha256)? { - cd - } else if let Some(cd) = signature.code_directory_for_digest(DigestType::Sha1)? { - cd - } else if let Some(cd) = signature.code_directory()? { - cd - } else { - return Err(AppleCodesignError::BinaryNoCodeSignature); - }; - - let code_directory_blob = cd.to_blob_bytes()?; - - let designated_code_requirement = if let Some(requirements) = - signature.code_requirements()? - { - if let Some(designated) = requirements.requirements.get(&RequirementType::Designated) { - let req = designated.parse_expressions()?; - - Some(format!("{}", req[0])) - } else { - None - } - } else { - None - }; - - Ok(SignedMachOInfo { - code_directory_blob, - designated_code_requirement, - }) - } - - /// Resolve the parsed code directory from stored data. - pub fn code_directory(&self) -> Result>, AppleCodesignError> { - let blob = BlobData::from_blob_bytes(&self.code_directory_blob)?; - - if let BlobData::CodeDirectory(cd) = blob { - Ok(cd) - } else { - Err(AppleCodesignError::BinaryNoCodeSignature) - } - } - - /// Resolve the notarization ticket record name for this Mach-O file. - pub fn notarization_ticket_record_name(&self) -> Result { - let cd = self.code_directory()?; - - let digest_type: u8 = cd.digest_type.into(); - - let mut digest = cd.digest_with(cd.digest_type)?; - - // Digests appear to be truncated at 20 bytes / 40 characters. - digest.truncate(20); - - let digest = hex::encode(digest); - - // Unsure what the leading `2/` means. - Ok(format!("2/{}/{}", digest_type, digest)) - } -} - -/// Used to process individual files within a bundle. -/// -/// This abstraction lets entities like [CodeResourcesBuilder] drive the -/// installation of files into a new bundle. -pub trait BundleFileHandler { - /// Ensures a file (regular or symlink) is installed. - fn install_file(&self, file: &DirectoryBundleFile) -> Result<(), AppleCodesignError>; - - /// Sign a Mach-O file and ensure its new content is installed. - /// - /// Returns Mach-O metadata which will be recorded in - /// [crate::code_resources::CodeResources]. - fn sign_and_install_macho( - &self, - file: &DirectoryBundleFile, - ) -> Result; -} - -struct SingleBundleHandler<'a, 'key> { - settings: &'a SigningSettings<'key>, - dest_dir: PathBuf, -} - -impl<'a, 'key> BundleFileHandler for SingleBundleHandler<'a, 'key> { - fn install_file(&self, file: &DirectoryBundleFile) -> Result<(), AppleCodesignError> { - let source_path = file.absolute_path(); - let dest_path = self.dest_dir.join(file.relative_path()); - - if source_path != dest_path { - std::fs::create_dir_all( - dest_path - .parent() - .expect("parent directory should be available"), - )?; - - let metadata = source_path.symlink_metadata()?; - let mtime = filetime::FileTime::from_last_modification_time(&metadata); - - if let Some(target) = file - .symlink_target() - .map_err(AppleCodesignError::DirectoryBundle)? - { - info!( - "replicating symlink {} -> {}", - dest_path.display(), - target.display() - ); - create_symlink(&dest_path, target)?; - filetime::set_symlink_file_times( - &dest_path, - filetime::FileTime::from_last_access_time(&metadata), - mtime, - )?; - } else { - info!( - "copying file {} -> {}", - source_path.display(), - dest_path.display() - ); - std::fs::copy(&source_path, &dest_path)?; - filetime::set_file_mtime(&dest_path, mtime)?; - } - } - - Ok(()) - } - - fn sign_and_install_macho( - &self, - file: &DirectoryBundleFile, - ) -> Result { - info!("signing Mach-O file {}", file.relative_path().display()); - - let macho_data = std::fs::read(file.absolute_path())?; - let signer = MachOSigner::new(&macho_data)?; - - let mut settings = self - .settings - .as_bundle_macho_settings(file.relative_path().to_string_lossy().as_ref()); - - settings.import_settings_from_macho(&macho_data)?; - - // If there isn't a defined binary identifier, derive one from the file name so one is set - // 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 = file - .relative_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()); - - info!( - "Mach-O is missing binary identifier; setting to {} based on file name", - identifier - ); - settings.set_binary_identifier(SettingsScope::Main, identifier); - } - - let mut new_data = Vec::::with_capacity(macho_data.len() + 2_usize.pow(17)); - signer.write_signed_binary(&settings, &mut new_data)?; - - let dest_path = self.dest_dir.join(file.relative_path()); - - info!("writing Mach-O to {}", dest_path.display()); - write_macho_file(file.absolute_path(), &dest_path, &new_data)?; - - SignedMachOInfo::parse_data(&new_data) - } -} - -/// A primitive for signing a single Apple bundle. -/// -/// Unlike [BundleSigner], this type only signs a single bundle and is ignorant -/// about nested bundles. You probably want to use [BundleSigner] as the interface -/// for signing bundles, as failure to account for nested bundles can result in -/// signature verification errors. -pub struct SingleBundleSigner { - /// The bundle being signed. - bundle: DirectoryBundle, -} - -impl SingleBundleSigner { - /// Construct a new instance. - pub fn new(bundle: DirectoryBundle) -> Self { - Self { bundle } - } - - /// Write a signed bundle to the given directory. - pub fn write_signed_bundle( - &self, - dest_dir: impl AsRef, - settings: &SigningSettings, - ) -> Result { - let dest_dir = dest_dir.as_ref(); - - warn!( - "signing bundle at {} into {}", - self.bundle.root_dir().display(), - dest_dir.display() - ); - - // Frameworks are a bit special. - // - // Modern frameworks typically have a `Versions/` directory containing directories - // with the actual frameworks. These are the actual directories that are signed - not - // the top-most directory. In fact, the top-most `.framework` directory doesn't have any - // code signature elements at all and can effectively be ignored as far as signing - // is concerned. - // - // But even if there is a `Versions/` directory with nested bundles to sign, the top-level - // directory may have some symlinks. And those need to be preserved. In addition, there - // may be symlinks in `Versions/`. `Versions/Current` is common. - // - // Of course, if there is no `Versions/` directory, the top-level directory could be - // a valid framework warranting signing. - if self.bundle.package_type() == BundlePackageType::Framework { - if self.bundle.root_dir().join("Versions").is_dir() { - warn!("found a versioned framework; each version will be signed as its own bundle"); - - // But we still need to preserve files (hopefully just symlinks) outside the - // nested bundles under `Versions/`. Since we don't nest into child bundles - // here, it should be safe to handle each encountered file. - let handler = SingleBundleHandler { - dest_dir: dest_dir.to_path_buf(), - settings, - }; - - for file in self - .bundle - .files(false) - .map_err(AppleCodesignError::DirectoryBundle)? - { - handler.install_file(&file)?; - } - - return DirectoryBundle::new_from_path(dest_dir) - .map_err(AppleCodesignError::DirectoryBundle); - } else { - warn!("found an unversioned framework; signing like normal"); - } - } - - let dest_dir_root = dest_dir.to_path_buf(); - - let dest_dir = if self.bundle.shallow() { - dest_dir_root.clone() - } else { - dest_dir.join("Contents") - }; - - self.bundle - .identifier() - .map_err(AppleCodesignError::DirectoryBundle)? - .ok_or_else(|| AppleCodesignError::BundleNoIdentifier(self.bundle.info_plist_path()))?; - - let mut resources_digests = settings.all_digests(SettingsScope::Main); - - // State in the main executable can influence signing settings of the bundle. So examine - // it first. - - let main_exe = self - .bundle - .files(false) - .map_err(AppleCodesignError::DirectoryBundle)? - .into_iter() - .find(|f| matches!(f.is_main_executable(), Ok(true))); - - if let Some(exe) = &main_exe { - let macho_data = std::fs::read(exe.absolute_path())?; - let mach = MachFile::parse(&macho_data)?; - - for macho in mach.iter_macho() { - if let Some(targeting) = macho.find_targeting()? { - let sha256_version = targeting.platform.sha256_digest_support()?; - - if !sha256_version.matches(&targeting.minimum_os_version) - && resources_digests != vec![DigestType::Sha1, DigestType::Sha256] - { - info!("main executable targets OS requiring SHA-1 signatures; activating SHA-1 + SHA-256 signing"); - resources_digests = vec![DigestType::Sha1, DigestType::Sha256]; - break; - } - } - } - } - - warn!("collecting code resources files"); - - // The set of rules to use is determined by whether the bundle *can* have a - // `Resources/`, not whether it necessarily does. The exact rules for this are not - // known. Essentially we want to test for the result of CFBundleCopyResourcesDirectoryURL(). - // We assume that we can use the resources rules when there is a `Resources` directory - // (this seems obvious!) or when the bundle isn't shallow, as a non-shallow bundle should - // be an app bundle and app bundles can always have resources (we think). - let mut resources_builder = - if self.bundle.resolve_path("Resources").is_dir() || !self.bundle.shallow() { - CodeResourcesBuilder::default_resources_rules()? - } else { - CodeResourcesBuilder::default_no_resources_rules()? - }; - - // Ensure emitted digests match what we're configured to emit. - resources_builder.set_digests(resources_digests.into_iter()); - - // Exclude code signature files we'll write. - resources_builder.add_exclusion_rule(CodeResourcesRule::new("^_CodeSignature/")?.exclude()); - // Ignore notarization ticket. - resources_builder.add_exclusion_rule(CodeResourcesRule::new("^CodeResources$")?.exclude()); - - let handler = SingleBundleHandler { - dest_dir: dest_dir_root.clone(), - settings, - }; - - let mut info_plist_data = None; - - // Iterate files in this bundle and register as code resources. - // - // Traversing into nested bundles seems wrong but it is correct. The resources builder - // has rules to determine whether to process a path and assuming the rules and evaluation - // of them is correct, it is able to decide for itself how to handle a path. - // - // Furthermore, this behavior is needed as bundles can encapsulate signatures for nested - // bundles. For example, you could have a framework bundle with an embedded app bundle in - // `Resources/MyApp.app`! In this case, the framework's CodeResources encapsulates the - // content of `Resources/My.app` per the processing rules. - for file in self - .bundle - .files(true) - .map_err(AppleCodesignError::DirectoryBundle)? - { - // The main executable is special and handled below. - if file - .is_main_executable() - .map_err(AppleCodesignError::DirectoryBundle)? - { - continue; - } else if file.is_info_plist() { - // The Info.plist is digested specially. But it may also be handled by - // the resources handler. So always feed it through. - info!( - "{} is the Info.plist file; handling specially", - file.relative_path().display() - ); - resources_builder.process_file(&file, &handler)?; - info_plist_data = Some(std::fs::read(file.absolute_path())?); - } else { - resources_builder.process_file(&file, &handler)?; - } - } - - // Seal code directory digests of any nested bundles. - // - // Apple's tooling seems to only do this for some bundle type combinations. I'm - // not yet sure what the complete heuristic is. But we observed that frameworks - // don't appear to include digests of any nested app bundles. So we add that - // exclusion. We should figure out what the actual rules here... - if self.bundle.package_type() != BundlePackageType::Framework { - let dest_bundle = DirectoryBundle::new_from_path(&dest_dir) - .map_err(AppleCodesignError::DirectoryBundle)?; - - for (rel_path, nested_bundle) in dest_bundle - .nested_bundles(false) - .map_err(AppleCodesignError::DirectoryBundle)? - { - resources_builder.process_nested_bundle(&rel_path, &nested_bundle)?; - } - } - - // The resources are now sealed. Write out that XML file. - let code_resources_path = dest_dir.join("_CodeSignature").join("CodeResources"); - warn!( - "writing sealed resources to {}", - code_resources_path.display() - ); - std::fs::create_dir_all(code_resources_path.parent().unwrap())?; - let mut resources_data = Vec::::new(); - resources_builder.write_code_resources(&mut resources_data)?; - - { - let mut fh = std::fs::File::create(&code_resources_path)?; - fh.write_all(&resources_data)?; - } - - // Seal the main executable. - if let Some(exe) = main_exe { - warn!("signing main executable {}", exe.relative_path().display()); - - let macho_data = std::fs::read(exe.absolute_path())?; - let signer = MachOSigner::new(&macho_data)?; - - let mut settings = settings.clone(); - - settings.import_settings_from_macho(&macho_data)?; - - // The identifier for the main executable is defined in the bundle's Info.plist. - if let Some(ident) = self - .bundle - .identifier() - .map_err(AppleCodesignError::DirectoryBundle)? - { - info!("setting main executable binary identifier to {} (derived from CFBundleIdentifier in Info.plist)", ident); - settings.set_binary_identifier(SettingsScope::Main, ident); - } else { - info!("unable to determine binary identifier from bundle's Info.plist (CFBundleIdentifier not set?)"); - } - - settings.set_code_resources_data(SettingsScope::Main, resources_data); - - if let Some(info_plist_data) = info_plist_data { - settings.set_info_plist_data(SettingsScope::Main, info_plist_data); - } - - let mut new_data = Vec::::with_capacity(macho_data.len() + 2_usize.pow(17)); - signer.write_signed_binary(&settings, &mut new_data)?; - - let dest_path = dest_dir_root.join(exe.relative_path()); - info!("writing signed main executable to {}", dest_path.display()); - write_macho_file(exe.absolute_path(), &dest_path, &new_data)?; - } else { - warn!("bundle has no main executable to sign specially"); - } - - DirectoryBundle::new_from_path(&dest_dir_root).map_err(AppleCodesignError::DirectoryBundle) - } -} diff --git a/apple-codesign/src/certificate.rs b/apple-codesign/src/certificate.rs deleted file mode 100644 index 62334681e..000000000 --- a/apple-codesign/src/certificate.rs +++ /dev/null @@ -1,1765 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Functionality related to certificates. - -use { - crate::{apple_certificates::KnownCertificate, error::AppleCodesignError}, - bcder::{ - encode::{PrimitiveContent, Values}, - ConstOid, Oid, - }, - bytes::Bytes, - std::{ - fmt::{Display, Formatter}, - str::FromStr, - }, - x509_certificate::{ - certificate::KeyUsage, rfc4519::OID_COUNTRY_NAME, CapturedX509Certificate, - InMemorySigningKeyPair, KeyAlgorithm, X509CertificateBuilder, - }, -}; - -/// Extended Key Usage extension. -/// -/// 2.5.29.37 -const OID_EXTENDED_KEY_USAGE: ConstOid = Oid(&[85, 29, 37]); - -/// Extended Key Usage purpose for code signing. -/// -/// 1.3.6.1.5.5.7.3.3 -const OID_EKU_PURPOSE_CODE_SIGNING: ConstOid = Oid(&[43, 6, 1, 5, 5, 7, 3, 3]); - -/// Extended Key Usage for purpose of `Safari Developer`. -/// -/// 1.2.840.113635.100.4.8 -const OID_EKU_PURPOSE_SAFARI_DEVELOPER: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 4, 8]); - -/// Extended Key Usage for purpose of `3rd Party Mac Developer Installer`. -/// -/// 1.2.840.113635.100.4.9 -const OID_EKU_PURPOSE_3RD_PARTY_MAC_DEVELOPER_INSTALLER: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 4, 9]); - -/// Extended Key Usage for purpose of `Developer ID Installer`. -/// -/// 1.2.840.113635.100.4.13 -const OID_EKU_PURPOSE_DEVELOPER_ID_INSTALLER: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 4, 13]); - -/// All OIDs known for extended key usage. -const ALL_OID_EKUS: &[&ConstOid; 4] = &[ - &OID_EKU_PURPOSE_CODE_SIGNING, - &OID_EKU_PURPOSE_SAFARI_DEVELOPER, - &OID_EKU_PURPOSE_3RD_PARTY_MAC_DEVELOPER_INSTALLER, - &OID_EKU_PURPOSE_DEVELOPER_ID_INSTALLER, -]; - -/// Extension for `Apple Signing`. -/// -/// 1.2.840.113635.100.6.1.1 -const OID_EXTENSION_APPLE_SIGNING: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 1]); - -/// Extension for `iPhone Developer`. -/// -/// 1.2.840.113635.100.6.1.2 -const OID_EXTENSION_IPHONE_DEVELOPER: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 2]); - -/// Extension for `Apple iPhone OS Application Signing` -/// -/// 1.2.840.113635.100.6.1.3 -const OID_EXTENSION_IPHONE_OS_APPLICATION_SIGNING: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 3]); - -/// Extension for `Apple Developer Certificate (Submission)`. -/// -/// May also be referred to as `iPhone Distribution`. -/// -/// 1.2.840.113635.100.6.1.4 -const OID_EXTENSION_APPLE_DEVELOPER_CERTIFICATE_SUBMISSION: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 4]); - -/// Extension for `Safari Developer`. -/// -/// 1.2.840.113635.100.6.1.5 -const OID_EXTENSION_SAFARI_DEVELOPER: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 5]); - -/// Extension for `Apple iPhone OS VPN Signing` -/// -/// 1.2.840.113635.100.6.1.6 -const OID_EXTENSION_IPHONE_OS_VPN_SIGNING: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 6]); - -/// Extension for `Apple Mac App Signing (Development)`. -/// -/// May also appear as `3rd Party Mac Developer Application`. -/// -/// 1.2.840.113635.100.6.1.7 -const OID_EXTENSION_APPLE_MAC_APP_SIGNING_DEVELOPMENT: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 7]); - -/// Extension for `Apple Mac App Signing Submission`. -/// -/// 1.2.840.113635.100.6.1.8 -const OID_EXTENSION_APPLE_MAC_APP_SIGNING_SUBMISSION: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 8]); - -/// Extension for `Mac App Store Code Signing`. -/// -/// 1.2.840.113635.100.6.1.9 -const OID_EXTENSION_APPLE_MAC_APP_STORE_CODE_SIGNING: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 9]); - -/// Extension for `Mac App Store Installer Signing`. -/// -/// 1.2.840.113635.100.6.1.10 -const OID_EXTENSION_APPLE_MAC_APP_STORE_INSTALLER_SIGNING: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 10]); - -// 1.2.840.113635.100.6.1.11 is unknown. - -/// Extension for `Mac Developer`. -/// -/// 1.2.840.113635.100.6.1.12 -const OID_EXTENSION_MAC_DEVELOPER: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 12]); - -/// Extension for `Developer ID Application`. -/// -/// 1.2.840.113635.100.6.1.13 -const OID_EXTENSION_DEVELOPER_ID_APPLICATION: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 13]); - -/// Extension for `Developer ID Installer`. -/// -/// 1.2.840.113635.100.6.1.14 -const OID_EXTENSION_DEVELOPER_ID_INSTALLER: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 14]); - -// 1.2.840.113635.100.6.1.15 looks to have something to do with core OS functionality, -// as it appears in search results for hacking Apple OS booting. - -/// Extension for `Apple Pay Passbook Signing` -/// -/// 1.2.840.113635.100.6.1.16 -const OID_EXTENSION_PASSBOOK_SIGNING: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 16]); - -/// Extension for `Web Site Push Notifications Signing` -/// -/// 1.2.840.113635.100.6.1.17 -const OID_EXTENSION_WEBSITE_PUSH_NOTIFICATION_SIGNING: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 17]); - -/// Extension for `Developer ID Kernel`. -/// -/// 1.2.840.113635.100.6.1.18 -const OID_EXTENSION_DEVELOPER_ID_KERNEL: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 18]); - -/// Extension for `Developer ID Date`. -/// -/// This OID doesn't have a description in Apple tooling. But it -/// holds a UtcDate (with hours, minutes, and seconds all set to 0) and seems to -/// denote a date constraint to apply to validation. This is likely used -/// to validating timestamping constrains for certificate validity. -/// -/// 1.2.840.113635.100.6.1.33 -const OID_EXTENSION_DEVELOPER_ID_DATE: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 33]); - -/// Extension for `TestFlight`. -/// -/// 1.2.840.113635.100.6.1.25.1 -const OID_EXTENSION_TEST_FLIGHT: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 25, 1]); - -/// All OIDs associated with non Certificate Authority extensions. -const ALL_OID_NON_CA_EXTENSIONS: &[&ConstOid; 18] = &[ - &OID_EXTENSION_APPLE_SIGNING, - &OID_EXTENSION_IPHONE_DEVELOPER, - &OID_EXTENSION_IPHONE_OS_APPLICATION_SIGNING, - &OID_EXTENSION_APPLE_DEVELOPER_CERTIFICATE_SUBMISSION, - &OID_EXTENSION_SAFARI_DEVELOPER, - &OID_EXTENSION_IPHONE_OS_VPN_SIGNING, - &OID_EXTENSION_APPLE_MAC_APP_SIGNING_DEVELOPMENT, - &OID_EXTENSION_APPLE_MAC_APP_SIGNING_SUBMISSION, - &OID_EXTENSION_APPLE_MAC_APP_STORE_CODE_SIGNING, - &OID_EXTENSION_APPLE_MAC_APP_STORE_INSTALLER_SIGNING, - &OID_EXTENSION_MAC_DEVELOPER, - &OID_EXTENSION_DEVELOPER_ID_APPLICATION, - &OID_EXTENSION_DEVELOPER_ID_INSTALLER, - &OID_EXTENSION_PASSBOOK_SIGNING, - &OID_EXTENSION_WEBSITE_PUSH_NOTIFICATION_SIGNING, - &OID_EXTENSION_DEVELOPER_ID_KERNEL, - &OID_EXTENSION_DEVELOPER_ID_DATE, - &OID_EXTENSION_TEST_FLIGHT, -]; - -/// UserID. -/// -/// 0.9.2342.19200300.100.1.1 -pub const OID_USER_ID: ConstOid = Oid(&[9, 146, 38, 137, 147, 242, 44, 100, 1, 1]); - -/// OID used for email address in RDN in Apple generated code signing certificates. -const OID_EMAIL_ADDRESS: ConstOid = Oid(&[42, 134, 72, 134, 247, 13, 1, 9, 1]); - -/// Apple Worldwide Developer Relations. -/// -/// 1.2.840.113635.100.6.2.1 -const OID_CA_EXTENSION_APPLE_WORLDWIDE_DEVELOPER_RELATIONS: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 2, 1]); - -/// Apple Application Integration. -/// -/// 1.2.840.113635.100.6.2.3 -const OID_CA_EXTENSION_APPLE_APPLICATION_INTEGRATION: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 2, 3]); - -/// Developer ID Certification Authority -/// -/// 1.2.840.113635.100.6.2.6 -const OID_CA_EXTENSION_DEVELOPER_ID: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 2, 6]); - -/// Apple Timestamp. -/// -/// 1.2.840.113635.100.6.2.9 -const OID_CA_EXTENSION_APPLE_TIMESTAMP: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 2, 9]); - -/// Developer Authentication Certification Authority. -/// -/// 1.2.840.113635.100.6.2.11 -const OID_CA_EXTENSION_DEVELOPER_AUTHENTICATION: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 2, 11]); - -/// Apple Application Integration CA - G3 -/// -/// 1.2.840.113635.100.6.2.14 -const OID_CA_EXTENSION_APPLE_APPLICATION_INTEGRATION_G3: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 2, 14]); - -/// Apple Worldwide Developer Relations CA - G2 -/// -/// 1.2.840.113635.100.6.2.15 -const OID_CA_EXTENSION_APPLE_WORLDWIDE_DEVELOPER_RELATIONS_G2: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 2, 15]); - -/// Apple Software Update Certification. -/// -/// 1.2.840.113635.100.6.2.19 -const OID_CA_EXTENSION_APPLE_SOFTWARE_UPDATE_CERTIFICATION: ConstOid = - Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 2, 19]); - -const ALL_OID_CA_EXTENSIONS: &[&ConstOid; 8] = &[ - &OID_CA_EXTENSION_APPLE_WORLDWIDE_DEVELOPER_RELATIONS, - &OID_CA_EXTENSION_APPLE_APPLICATION_INTEGRATION, - &OID_CA_EXTENSION_DEVELOPER_ID, - &OID_CA_EXTENSION_APPLE_TIMESTAMP, - &OID_CA_EXTENSION_DEVELOPER_AUTHENTICATION, - &OID_CA_EXTENSION_APPLE_APPLICATION_INTEGRATION_G3, - &OID_CA_EXTENSION_APPLE_WORLDWIDE_DEVELOPER_RELATIONS_G2, - &OID_CA_EXTENSION_APPLE_SOFTWARE_UPDATE_CERTIFICATION, -]; - -/// Describes the type of code signing that a certificate is authorized to perform. -/// -/// Code signing certificates are issued with extended key usage (EKU) attributes -/// denoting what that certificate will be used for. They basically say *I'm authorized -/// to sign X*. -/// -/// This type describes the different code signing key usages defined on Apple -/// platforms. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ExtendedKeyUsagePurpose { - /// Code signing. - CodeSigning, - - /// Safari Developer. - SafariDeveloper, - - /// 3rd Party Mac Developer Installer Packaging Signing. - /// - /// The certificate can be used to sign Mac installer packages. - ThirdPartyMacDeveloperInstaller, - - /// Developer ID Installer. - DeveloperIdInstaller, -} - -impl ExtendedKeyUsagePurpose { - /// Obtain all variants of this enumeration. - pub fn all() -> Vec { - vec![ - Self::CodeSigning, - Self::SafariDeveloper, - Self::ThirdPartyMacDeveloperInstaller, - Self::DeveloperIdInstaller, - ] - } - - pub fn all_oids() -> &'static [&'static ConstOid] { - ALL_OID_EKUS - } - - pub fn as_oid(&self) -> ConstOid { - match self { - Self::CodeSigning => OID_EKU_PURPOSE_CODE_SIGNING, - Self::SafariDeveloper => OID_EKU_PURPOSE_SAFARI_DEVELOPER, - Self::ThirdPartyMacDeveloperInstaller => { - OID_EKU_PURPOSE_3RD_PARTY_MAC_DEVELOPER_INSTALLER - } - Self::DeveloperIdInstaller => OID_EKU_PURPOSE_DEVELOPER_ID_INSTALLER, - } - } -} - -impl Display for ExtendedKeyUsagePurpose { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ExtendedKeyUsagePurpose::CodeSigning => f.write_str("Code Signing"), - ExtendedKeyUsagePurpose::SafariDeveloper => f.write_str("Safari Developer"), - ExtendedKeyUsagePurpose::ThirdPartyMacDeveloperInstaller => { - f.write_str("3rd Party Mac Developer Installer Packaging Signing") - } - ExtendedKeyUsagePurpose::DeveloperIdInstaller => f.write_str("Developer ID Installer"), - } - } -} - -impl TryFrom<&Oid> for ExtendedKeyUsagePurpose { - type Error = AppleCodesignError; - - fn try_from(oid: &Oid) -> Result { - // Surely there is a way to use `match`. But the `Oid` type is a bit wonky. - if oid.as_ref() == OID_EKU_PURPOSE_CODE_SIGNING.as_ref() { - Ok(Self::CodeSigning) - } else if oid.as_ref() == OID_EKU_PURPOSE_SAFARI_DEVELOPER.as_ref() { - Ok(Self::SafariDeveloper) - } else if oid.as_ref() == OID_EKU_PURPOSE_3RD_PARTY_MAC_DEVELOPER_INSTALLER.as_ref() { - Ok(Self::ThirdPartyMacDeveloperInstaller) - } else if oid.as_ref() == OID_EKU_PURPOSE_DEVELOPER_ID_INSTALLER.as_ref() { - Ok(Self::DeveloperIdInstaller) - } else { - Err(AppleCodesignError::OidIsntCertificateAuthority) - } - } -} - -/// Describes one of the many X.509 certificate extensions found on Apple code signing certificates. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum CodeSigningCertificateExtension { - /// Apple Signing. - /// - /// (Appears to be deprecated). - AppleSigning, - - /// iPhone Developer. - IPhoneDeveloper, - - /// Apple iPhone OS Application Signing. - IPhoneOsApplicationSigning, - - /// Apple Developer Certificate (Submission). - /// - /// May also be referred to as `iPhone Distribution`. - AppleDeveloperCertificateSubmission, - - /// Safari Developer. - SafariDeveloper, - - /// Apple iPhone OS VPN Signing. - IPhoneOsVpnSigning, - - /// Apple Mac App Signing (Development). - /// - /// Also known as `3rd Party Mac Developer Application`. - AppleMacAppSigningDevelopment, - - /// Apple Mac App Signing Submission. - AppleMacAppSigningSubmission, - - /// Mac App Store Code Signing. - AppleMacAppStoreCodeSigning, - - /// Mac App Store Installer Signing. - AppleMacAppStoreInstallerSigning, - - /// Mac Developer. - MacDeveloper, - - /// Developer ID Application. - DeveloperIdApplication, - - /// Developer ID Date. - DeveloperIdDate, - - /// Developer ID Installer. - DeveloperIdInstaller, - - /// Apple Pay Passbook Signing. - ApplePayPassbookSigning, - - /// Web Site Push Notifications Signing. - WebsitePushNotificationSigning, - - /// Developer ID Kernel. - DeveloperIdKernel, - - /// TestFlight. - TestFlight, -} - -impl CodeSigningCertificateExtension { - /// Obtain all variants of this enumeration. - pub fn all() -> Vec { - vec![ - Self::AppleSigning, - Self::IPhoneDeveloper, - Self::IPhoneOsApplicationSigning, - Self::AppleDeveloperCertificateSubmission, - Self::SafariDeveloper, - Self::IPhoneOsVpnSigning, - Self::AppleMacAppSigningDevelopment, - Self::AppleMacAppSigningSubmission, - Self::AppleMacAppStoreCodeSigning, - Self::AppleMacAppStoreInstallerSigning, - Self::MacDeveloper, - Self::DeveloperIdApplication, - Self::DeveloperIdDate, - Self::DeveloperIdInstaller, - Self::ApplePayPassbookSigning, - Self::WebsitePushNotificationSigning, - Self::DeveloperIdKernel, - Self::TestFlight, - ] - } - - /// All OIDs known to be extensions in code signing certificates. - pub fn all_oids() -> &'static [&'static ConstOid] { - ALL_OID_NON_CA_EXTENSIONS - } - - pub fn as_oid(&self) -> ConstOid { - match self { - Self::AppleSigning => OID_EXTENSION_APPLE_SIGNING, - Self::IPhoneDeveloper => OID_EXTENSION_IPHONE_DEVELOPER, - Self::IPhoneOsApplicationSigning => OID_EXTENSION_IPHONE_OS_APPLICATION_SIGNING, - Self::AppleDeveloperCertificateSubmission => { - OID_EXTENSION_APPLE_DEVELOPER_CERTIFICATE_SUBMISSION - } - Self::SafariDeveloper => OID_EXTENSION_SAFARI_DEVELOPER, - Self::IPhoneOsVpnSigning => OID_EXTENSION_IPHONE_OS_VPN_SIGNING, - Self::AppleMacAppSigningDevelopment => OID_EXTENSION_APPLE_MAC_APP_SIGNING_DEVELOPMENT, - Self::AppleMacAppSigningSubmission => OID_EXTENSION_APPLE_MAC_APP_SIGNING_SUBMISSION, - Self::AppleMacAppStoreCodeSigning => OID_EXTENSION_APPLE_MAC_APP_STORE_CODE_SIGNING, - Self::AppleMacAppStoreInstallerSigning => { - OID_EXTENSION_APPLE_MAC_APP_STORE_INSTALLER_SIGNING - } - Self::MacDeveloper => OID_EXTENSION_MAC_DEVELOPER, - Self::DeveloperIdApplication => OID_EXTENSION_DEVELOPER_ID_APPLICATION, - Self::DeveloperIdDate => OID_EXTENSION_DEVELOPER_ID_DATE, - Self::DeveloperIdInstaller => OID_EXTENSION_DEVELOPER_ID_INSTALLER, - Self::ApplePayPassbookSigning => OID_EXTENSION_PASSBOOK_SIGNING, - Self::WebsitePushNotificationSigning => OID_EXTENSION_WEBSITE_PUSH_NOTIFICATION_SIGNING, - Self::DeveloperIdKernel => OID_EXTENSION_DEVELOPER_ID_KERNEL, - Self::TestFlight => OID_EXTENSION_TEST_FLIGHT, - } - } -} - -impl Display for CodeSigningCertificateExtension { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - CodeSigningCertificateExtension::AppleSigning => f.write_str("Apple Signing"), - CodeSigningCertificateExtension::IPhoneDeveloper => f.write_str("iPhone Developer"), - CodeSigningCertificateExtension::IPhoneOsApplicationSigning => { - f.write_str("Apple iPhone OS Application Signing") - } - CodeSigningCertificateExtension::AppleDeveloperCertificateSubmission => { - f.write_str("Apple Developer Certificate (Submission)") - } - CodeSigningCertificateExtension::SafariDeveloper => f.write_str("Safari Developer"), - CodeSigningCertificateExtension::IPhoneOsVpnSigning => { - f.write_str("Apple iPhone OS VPN Signing") - } - CodeSigningCertificateExtension::AppleMacAppSigningDevelopment => { - f.write_str("Apple Mac App Signing (Development)") - } - CodeSigningCertificateExtension::AppleMacAppSigningSubmission => { - f.write_str("Apple Mac App Signing Submission") - } - CodeSigningCertificateExtension::AppleMacAppStoreCodeSigning => { - f.write_str("Mac App Store Code Signing") - } - CodeSigningCertificateExtension::AppleMacAppStoreInstallerSigning => { - f.write_str("Mac App Store Installer Signing") - } - CodeSigningCertificateExtension::MacDeveloper => f.write_str("Mac Developer"), - CodeSigningCertificateExtension::DeveloperIdApplication => { - f.write_str("Developer ID Application") - } - CodeSigningCertificateExtension::DeveloperIdDate => f.write_str("Developer ID Date"), - CodeSigningCertificateExtension::DeveloperIdInstaller => { - f.write_str("Developer ID Installer") - } - CodeSigningCertificateExtension::ApplePayPassbookSigning => { - f.write_str("Apple Pay Passbook Signing") - } - CodeSigningCertificateExtension::WebsitePushNotificationSigning => { - f.write_str("Web Site Push Notifications Signing") - } - CodeSigningCertificateExtension::DeveloperIdKernel => { - f.write_str("Developer ID Kernel") - } - CodeSigningCertificateExtension::TestFlight => f.write_str("TestFlight"), - } - } -} - -impl TryFrom<&Oid> for CodeSigningCertificateExtension { - type Error = AppleCodesignError; - - fn try_from(oid: &Oid) -> Result { - // Surely there is a way to use `match`. But the `Oid` type is a bit wonky. - let o = oid.as_ref(); - - if o == OID_EXTENSION_APPLE_SIGNING.as_ref() { - Ok(Self::AppleSigning) - } else if o == OID_EXTENSION_IPHONE_DEVELOPER.as_ref() { - Ok(Self::IPhoneDeveloper) - } else if o == OID_EXTENSION_IPHONE_OS_APPLICATION_SIGNING.as_ref() { - Ok(Self::IPhoneOsApplicationSigning) - } else if o == OID_EXTENSION_APPLE_DEVELOPER_CERTIFICATE_SUBMISSION.as_ref() { - Ok(Self::AppleDeveloperCertificateSubmission) - } else if o == OID_EXTENSION_SAFARI_DEVELOPER.as_ref() { - Ok(Self::SafariDeveloper) - } else if o == OID_EXTENSION_IPHONE_OS_VPN_SIGNING.as_ref() { - Ok(Self::IPhoneOsVpnSigning) - } else if o == OID_EXTENSION_APPLE_MAC_APP_SIGNING_DEVELOPMENT.as_ref() { - Ok(Self::AppleMacAppSigningDevelopment) - } else if o == OID_EXTENSION_APPLE_MAC_APP_SIGNING_SUBMISSION.as_ref() { - Ok(Self::AppleMacAppSigningSubmission) - } else if o == OID_EXTENSION_APPLE_MAC_APP_STORE_CODE_SIGNING.as_ref() { - Ok(Self::AppleMacAppStoreCodeSigning) - } else if o == OID_EXTENSION_APPLE_MAC_APP_STORE_INSTALLER_SIGNING.as_ref() { - Ok(Self::AppleMacAppStoreInstallerSigning) - } else if o == OID_EXTENSION_MAC_DEVELOPER.as_ref() { - Ok(Self::MacDeveloper) - } else if o == OID_EXTENSION_DEVELOPER_ID_APPLICATION.as_ref() { - Ok(Self::DeveloperIdApplication) - } else if o == OID_EXTENSION_DEVELOPER_ID_INSTALLER.as_ref() { - Ok(Self::DeveloperIdInstaller) - } else if o == OID_EXTENSION_PASSBOOK_SIGNING.as_ref() { - Ok(Self::ApplePayPassbookSigning) - } else if o == OID_EXTENSION_WEBSITE_PUSH_NOTIFICATION_SIGNING.as_ref() { - Ok(Self::WebsitePushNotificationSigning) - } else if o == OID_EXTENSION_DEVELOPER_ID_KERNEL.as_ref() { - Ok(Self::DeveloperIdKernel) - } else if o == OID_EXTENSION_DEVELOPER_ID_DATE.as_ref() { - Ok(Self::DeveloperIdDate) - } else if o == OID_EXTENSION_TEST_FLIGHT.as_ref() { - Ok(Self::TestFlight) - } else { - Err(AppleCodesignError::OidIsntCodeSigningExtension) - } - } -} - -/// Denotes specific certificate extensions on Apple certificate authority certificates. -/// -/// Apple's CA certificates have extensions that appear to identify the role of -/// that CA. This enumeration defines those. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum CertificateAuthorityExtension { - /// Apple Worldwide Developer Relations. - /// - /// An intermediate CA. - AppleWorldwideDeveloperRelations, - - /// Apple Application Integration. - AppleApplicationIntegration, - - /// Developer ID Certification Authority. - DeveloperId, - - /// Apple Timestamp. - AppleTimestamp, - - /// Developer Authentication Certification Authority. - DeveloperAuthentication, - - /// Application Application Integration CA - G3. - AppleApplicationIntegrationG3, - - /// Apple Worldwide Developer Relations CA - G2. - AppleWorldwideDeveloperRelationsG2, - - /// Apple Software Update Certification. - AppleSoftwareUpdateCertification, -} - -impl CertificateAuthorityExtension { - /// Obtain all variants of this enumeration. - pub fn all() -> Vec { - vec![ - Self::AppleWorldwideDeveloperRelations, - Self::AppleApplicationIntegration, - Self::DeveloperId, - Self::AppleTimestamp, - Self::DeveloperAuthentication, - Self::AppleApplicationIntegrationG3, - Self::AppleWorldwideDeveloperRelationsG2, - Self::AppleSoftwareUpdateCertification, - ] - } - - /// All the known OIDs constituting Apple CA extensions. - pub fn all_oids() -> &'static [&'static ConstOid] { - ALL_OID_CA_EXTENSIONS - } - - pub fn as_oid(&self) -> ConstOid { - match self { - Self::AppleWorldwideDeveloperRelations => { - OID_CA_EXTENSION_APPLE_WORLDWIDE_DEVELOPER_RELATIONS - } - Self::AppleApplicationIntegration => OID_CA_EXTENSION_APPLE_APPLICATION_INTEGRATION, - Self::DeveloperId => OID_CA_EXTENSION_DEVELOPER_ID, - Self::AppleTimestamp => OID_CA_EXTENSION_APPLE_TIMESTAMP, - Self::DeveloperAuthentication => OID_CA_EXTENSION_DEVELOPER_AUTHENTICATION, - Self::AppleApplicationIntegrationG3 => { - OID_CA_EXTENSION_APPLE_APPLICATION_INTEGRATION_G3 - } - Self::AppleWorldwideDeveloperRelationsG2 => { - OID_CA_EXTENSION_APPLE_WORLDWIDE_DEVELOPER_RELATIONS_G2 - } - Self::AppleSoftwareUpdateCertification => { - OID_CA_EXTENSION_APPLE_SOFTWARE_UPDATE_CERTIFICATION - } - } - } -} - -impl Display for CertificateAuthorityExtension { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - CertificateAuthorityExtension::AppleWorldwideDeveloperRelations => { - f.write_str("Apple Worldwide Developer Relations") - } - CertificateAuthorityExtension::AppleApplicationIntegration => { - f.write_str("Apple Application Integration") - } - CertificateAuthorityExtension::DeveloperId => { - f.write_str("Developer ID Certification Authority") - } - CertificateAuthorityExtension::AppleTimestamp => f.write_str("Apple Timestamp"), - CertificateAuthorityExtension::DeveloperAuthentication => { - f.write_str("Developer Authentication Certification Authority") - } - CertificateAuthorityExtension::AppleApplicationIntegrationG3 => { - f.write_str("Application Application Integration CA - G3") - } - CertificateAuthorityExtension::AppleWorldwideDeveloperRelationsG2 => { - f.write_str("Apple Worldwide Developer Relations CA - G2") - } - CertificateAuthorityExtension::AppleSoftwareUpdateCertification => { - f.write_str("Apple Software Update Certification") - } - } - } -} - -impl TryFrom<&Oid> for CertificateAuthorityExtension { - type Error = AppleCodesignError; - - fn try_from(oid: &Oid) -> Result { - // Surely there is a way to use `match`. But the `Oid` type is a bit wonky. - if oid.as_ref() == OID_CA_EXTENSION_APPLE_WORLDWIDE_DEVELOPER_RELATIONS.as_ref() { - Ok(Self::AppleWorldwideDeveloperRelations) - } else if oid.as_ref() == OID_CA_EXTENSION_APPLE_APPLICATION_INTEGRATION.as_ref() { - Ok(Self::AppleApplicationIntegration) - } else if oid.as_ref() == OID_CA_EXTENSION_DEVELOPER_ID.as_ref() { - Ok(Self::DeveloperId) - } else if oid.as_ref() == OID_CA_EXTENSION_APPLE_TIMESTAMP.as_ref() { - Ok(Self::AppleTimestamp) - } else if oid.as_ref() == OID_CA_EXTENSION_DEVELOPER_AUTHENTICATION.as_ref() { - Ok(Self::DeveloperAuthentication) - } else if oid.as_ref() == OID_CA_EXTENSION_APPLE_APPLICATION_INTEGRATION_G3.as_ref() { - Ok(Self::AppleApplicationIntegrationG3) - } else if oid.as_ref() == OID_CA_EXTENSION_APPLE_WORLDWIDE_DEVELOPER_RELATIONS_G2.as_ref() { - Ok(Self::AppleWorldwideDeveloperRelationsG2) - } else if oid.as_ref() == OID_CA_EXTENSION_APPLE_SOFTWARE_UPDATE_CERTIFICATION.as_ref() { - Ok(Self::AppleSoftwareUpdateCertification) - } else { - Err(AppleCodesignError::OidIsntCertificateAuthority) - } - } -} - -/// Describes combinations of certificate extensions for Apple code signing certificates. -/// -/// Code signing certificates contain various X.509 extensions denoting them for -/// code signing. -/// -/// This type represents various common extensions as used on Apple platforms. -/// -/// Typically, you'll want to apply at most one of these extensions to a -/// new certificate in order to mark it as compatible for code signing. -/// -/// This type essentially encapsulates the logic for handling of different -/// "profiles" attached to the different code signing certificates that Apple -/// issues. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum CertificateProfile { - /// Mac Installer Distribution. - /// - /// In `Keychain Access.app`, this might render as `3rd Party Mac Developer Installer`. - /// - /// Certificates are marked for EKU with `3rd Party Developer Installer Package - /// Signing`. - /// - /// They also have the `Apple Mac App Signing (Submission)` extension. - /// - /// Typically issued by `Apple Worldwide Developer Relations Certificate - /// Authority`. - MacInstallerDistribution, - - /// Apple Distribution. - /// - /// Certificates are marked for EKU with `Code Signing`. They also have - /// extensions `Apple Mac App Signing (Development)` and - /// `Apple Developer Certificate (Submission)`. - /// - /// Typically issued by `Apple Worldwide Developer Relations Certificate Authority`. - AppleDistribution, - - /// Apple Development. - /// - /// Certificates are marked for EKU with `Code Signing`. They also have - /// extensions `Apple Developer Certificate (Development)` and - /// `Mac Developer`. - /// - /// Typically issued by `Apple Worldwide Developer Relations Certificate - /// Authority`. - AppleDevelopment, - - /// Developer ID Application. - /// - /// Certificates are marked for EKU with `Code Signing`. They also have - /// extensions for `Developer ID Application` and `Developer ID Date`. - DeveloperIdApplication, - - /// Developer ID Installer. - /// - /// Certificates are marked for EKU with `Developer ID Application`. They also - /// have extensions `Developer ID Installer` and `Developer ID Date`. - DeveloperIdInstaller, -} - -impl CertificateProfile { - pub fn all() -> &'static [Self] { - &[ - Self::MacInstallerDistribution, - Self::AppleDistribution, - Self::AppleDevelopment, - Self::DeveloperIdApplication, - Self::DeveloperIdInstaller, - ] - } - - /// Obtain the string values that variants are recognized as. - pub fn str_names() -> &'static [&'static str] { - &[ - "mac-installer-distribution", - "apple-distribution", - "apple-development", - "developer-id-application", - "developer-id-installer", - ] - } -} - -impl Display for CertificateProfile { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - CertificateProfile::MacInstallerDistribution => { - f.write_str("mac-installer-distribution") - } - CertificateProfile::AppleDistribution => f.write_str("apple-distribution"), - CertificateProfile::AppleDevelopment => f.write_str("apple-development"), - CertificateProfile::DeveloperIdApplication => f.write_str("developer-id-application"), - CertificateProfile::DeveloperIdInstaller => f.write_str("developer-id-installer"), - } - } -} - -impl FromStr for CertificateProfile { - type Err = AppleCodesignError; - - fn from_str(s: &str) -> Result { - match s { - "apple-distribution" => Ok(Self::AppleDistribution), - "apple-development" => Ok(Self::AppleDevelopment), - "developer-id-application" => Ok(Self::DeveloperIdApplication), - "developer-id-installer" => Ok(Self::DeveloperIdInstaller), - "mac-installer-distribution" => Ok(Self::MacInstallerDistribution), - _ => Err(AppleCodesignError::UnknownCertificateProfile(s.to_string())), - } - } -} - -/// Extends functionality of [CapturedX509Certificate] with Apple specific certificate knowledge. -pub trait AppleCertificate: Sized { - /// Whether this is a known Apple root certificate authority. - /// - /// We define this criteria as a certificate in our built-in list of known - /// Apple certificates that has the same subject and issuer Names. - fn is_apple_root_ca(&self) -> bool; - - /// Whether this is a known Apple intermediate certificate authority. - /// - /// This is similar to [Self::is_apple_root_ca] except it doesn't match against - /// known self-signed Apple certificates. - fn is_apple_intermediate_ca(&self) -> bool; - - /// Find a [CertificateAuthorityExtension] present on this certificate. - /// - /// If this returns Some(T), the certificate says it is an Apple certificate - /// whose role is issuing other certificates using for signing things. - /// - /// This function does not perform trust validation that the underlying - /// certificate is a legitimate Apple issued certificate: just that it has - /// the desired property. - fn apple_ca_extension(&self) -> Option; - - /// Obtain all of Apple's [ExtendedKeyUsagePurpose] in this certificate. - fn apple_extended_key_usage_purposes(&self) -> Vec; - - /// Obtain all of Apple's [CodeSigningCertificateExtension] in this certificate. - fn apple_code_signing_extensions(&self) -> Vec; - - /// Attempt to guess the [CertificateProfile] associated with this certificate. - /// - /// This keys off present certificate extensions to guess which profile it - /// belongs to. Incorrect guesses are possible, which is why *guess* is in the - /// function name. - /// - /// Returns `None` if we don't think a [CertificateProfile] is associated with - /// this extension. - fn apple_guess_profile(&self) -> Option; - - /// Attempt to resolve the certificate issuer chain back to [AppleCertificate]. - /// - /// This is a glorified wrapper around [CapturedX509Certificate::resolve_signing_chain] - /// that filters matches against certificates in our known set of Apple - /// certificates and maps them back to our [KnownCertificate] Rust enumeration. - /// - /// False negatives (read: missing certificates) can be encountered if - /// we don't know about an Apple CA certificate. - fn apple_issuing_chain(&self) -> Vec; - - /// Whether this certificate chains back to a known Apple root certificate authority. - /// - /// This is true if the resolved certificate issuance chain (which is - /// confirmed via verifying the cryptographic signatures on certificates) - /// ands in a certificate that is known to be an Apple root CA. - fn chains_to_apple_root_ca(&self) -> bool; - - /// Obtain the chain of issuing certificates, back to a known Apple root. - /// - /// The returned chain starts with this certificate and ends with a known - /// Apple root certificate authority. None is returned if this certificate - /// doesn't appear to chain to a known Apple root CA. - fn apple_root_certificate_chain(&self) -> Option>; - - /// Attempt to resolve the *team id* of an Apple issued certificate. - /// - /// The *team id* is a value like `AB42XYZ789` that is attached to your - /// Apple Developer account. It seems to always be embedded in signing - /// certificates as the Organizational Unit field of the subject. So this - /// function is just a shortcut for retrieving that. - fn apple_team_id(&self) -> Option; -} - -impl AppleCertificate for CapturedX509Certificate { - fn is_apple_root_ca(&self) -> bool { - KnownCertificate::all_roots().contains(&self) - } - - fn is_apple_intermediate_ca(&self) -> bool { - KnownCertificate::all().contains(&self) && !KnownCertificate::all_roots().contains(&self) - } - - fn apple_ca_extension(&self) -> Option { - let cert: &x509_certificate::rfc5280::Certificate = self.as_ref(); - - cert.iter_extensions().find_map(|extension| { - if let Ok(value) = CertificateAuthorityExtension::try_from(&extension.id) { - Some(value) - } else { - None - } - }) - } - - fn apple_extended_key_usage_purposes(&self) -> Vec { - let cert: &x509_certificate::rfc5280::Certificate = self.as_ref(); - - cert.iter_extensions() - .filter_map(|extension| { - if extension.id.as_ref() == OID_EXTENDED_KEY_USAGE.as_ref() { - if let Some(oid) = extension.try_decode_sequence_single_oid() { - if let Ok(purpose) = ExtendedKeyUsagePurpose::try_from(&oid) { - Some(purpose) - } else { - None - } - } else { - None - } - } else { - None - } - }) - .collect::>() - } - - fn apple_code_signing_extensions(&self) -> Vec { - let cert: &x509_certificate::rfc5280::Certificate = self.as_ref(); - - cert.iter_extensions() - .filter_map(|extension| { - if let Ok(value) = CodeSigningCertificateExtension::try_from(&extension.id) { - Some(value) - } else { - None - } - }) - .collect::>() - } - - fn apple_guess_profile(&self) -> Option { - let ekus = self.apple_extended_key_usage_purposes(); - let signing = self.apple_code_signing_extensions(); - - // Some EKUs uniquely identify the certificate profile. We don't yet handle - // all EKUs because we don't have profiles defined for them. - // - // Ideally this logic stays in sync with apple_certificate_profile(). - if ekus.contains(&ExtendedKeyUsagePurpose::DeveloperIdInstaller) { - Some(CertificateProfile::DeveloperIdInstaller) - } else if ekus.contains(&ExtendedKeyUsagePurpose::ThirdPartyMacDeveloperInstaller) { - Some(CertificateProfile::MacInstallerDistribution) - // That's all the EKUs that have a 1:1 to CertificateProfile. Now look at - // code signing extensions. - } else if signing.contains(&CodeSigningCertificateExtension::DeveloperIdApplication) { - Some(CertificateProfile::DeveloperIdApplication) - } else if signing.contains(&CodeSigningCertificateExtension::IPhoneDeveloper) - && signing.contains(&CodeSigningCertificateExtension::MacDeveloper) - { - Some(CertificateProfile::AppleDevelopment) - } else if signing.contains(&CodeSigningCertificateExtension::AppleMacAppSigningDevelopment) - && signing - .contains(&CodeSigningCertificateExtension::AppleDeveloperCertificateSubmission) - { - Some(CertificateProfile::AppleDistribution) - } else { - None - } - } - - fn apple_issuing_chain(&self) -> Vec { - self.resolve_signing_chain(KnownCertificate::all().iter().copied()) - .into_iter() - .filter_map(|cert| KnownCertificate::try_from(cert).ok()) - .collect::>() - } - - fn chains_to_apple_root_ca(&self) -> bool { - if self.is_apple_root_ca() { - true - } else { - self.resolve_signing_chain(KnownCertificate::all().iter().copied()) - .into_iter() - .any(|cert| cert.is_apple_root_ca()) - } - } - - fn apple_root_certificate_chain(&self) -> Option> { - let mut chain = vec![self.clone()]; - - for cert in self.resolve_signing_chain(KnownCertificate::all().iter().copied()) { - chain.push(cert.clone()); - - if cert.is_apple_root_ca() { - break; - } - } - - if chain.last().unwrap().is_apple_root_ca() { - Some(chain) - } else { - None - } - } - - fn apple_team_id(&self) -> Option { - self.subject_name() - .find_first_attribute_string(Oid( - x509_certificate::rfc4519::OID_ORGANIZATIONAL_UNIT_NAME - .as_ref() - .into(), - )) - .unwrap_or(None) - } -} - -/// Extensions to [X509CertificateBuilder] specializing in Apple certificate behavior. -/// -/// Most callers should call [Self::apple_certificate_profile] to configure -/// a preset profile for the certificate being generated. After that - and it is -/// important it is after - call [Self::apple_subject] to define the subject -/// field. If you call this after registering code signing extensions, it -/// detects the appropriate format for the Common Name field. -pub trait AppleCertificateBuilder: Sized { - /// This functions defines common attributes on the certificate subject. - /// - /// `team_id` is your Apple team id. It is a short alphanumeric string. You - /// can find this at . - fn apple_subject( - &mut self, - team_id: &str, - person_name: &str, - country: &str, - ) -> Result<(), AppleCodesignError>; - - /// Add an email address to the certificate's subject name. - fn apple_email_address(&mut self, address: &str) -> Result<(), AppleCodesignError>; - - /// Add an [ExtendedKeyUsagePurpose] to this certificate. - fn apple_extended_key_usage( - &mut self, - usage: ExtendedKeyUsagePurpose, - ) -> Result<(), AppleCodesignError>; - - /// Add a certificate extension as defined by a [CodeSigningCertificateExtension] instance. - fn apple_code_signing_certificate_extension( - &mut self, - extension: CodeSigningCertificateExtension, - ) -> Result<(), AppleCodesignError>; - - /// Add a [CertificateProfile] to this builder. - /// - /// All certificate extensions relevant to this profile are added. - /// - /// This should be the first function you call after creating an instance - /// because other functions rely on the state that it sets. - fn apple_certificate_profile( - &mut self, - profile: CertificateProfile, - ) -> Result<(), AppleCodesignError>; - - /// Find code signing extensions that are currently registered. - fn apple_code_signing_extensions(&self) -> Vec; -} - -impl AppleCertificateBuilder for X509CertificateBuilder { - fn apple_subject( - &mut self, - team_id: &str, - person_name: &str, - country: &str, - ) -> Result<(), AppleCodesignError> { - // TODO the subject schema here isn't totally accurate. While OU does always - // appear to be the team id, the user id attribute can be something else. - // For example, for Apple Development, there are a similarly formatted yet - // different value. But the team id does still appear. - self.subject() - .append_utf8_string(Oid(OID_USER_ID.as_ref().into()), team_id) - .map_err(|e| AppleCodesignError::CertificateBuildError(format!("{:?}", e)))?; - - // Common Name is derived from the profile in use. - - let extensions = self.apple_code_signing_extensions(); - - let common_name = - if extensions.contains(&CodeSigningCertificateExtension::DeveloperIdApplication) { - format!("Developer ID Application: {} ({})", person_name, team_id) - } else if extensions.contains(&CodeSigningCertificateExtension::DeveloperIdInstaller) { - format!("Developer ID Installer: {} ({})", person_name, team_id) - } else if extensions - .contains(&CodeSigningCertificateExtension::AppleDeveloperCertificateSubmission) - { - format!("Apple Distribution: {} ({})", person_name, team_id) - } else if extensions - .contains(&CodeSigningCertificateExtension::AppleMacAppSigningSubmission) - { - format!( - "3rd Party Mac Developer Installer: {} ({})", - person_name, team_id - ) - } else if extensions.contains(&CodeSigningCertificateExtension::MacDeveloper) { - format!("Apple Development: {} ({})", person_name, team_id) - } else { - format!("{} ({})", person_name, team_id) - }; - - self.subject() - .append_common_name_utf8_string(&common_name) - .map_err(|e| AppleCodesignError::CertificateBuildError(format!("{:?}", e)))?; - - self.subject() - .append_organizational_unit_utf8_string(team_id) - .map_err(|e| AppleCodesignError::CertificateBuildError(format!("{:?}", e)))?; - - self.subject() - .append_organization_utf8_string(person_name) - .map_err(|e| AppleCodesignError::CertificateBuildError(format!("{:?}", e)))?; - - self.subject() - .append_printable_string(Oid(OID_COUNTRY_NAME.as_ref().into()), country) - .map_err(|e| AppleCodesignError::CertificateBuildError(format!("{:?}", e)))?; - - Ok(()) - } - - fn apple_email_address(&mut self, address: &str) -> Result<(), AppleCodesignError> { - self.subject() - .append_utf8_string(Oid(OID_EMAIL_ADDRESS.as_ref().into()), address) - .map_err(|e| AppleCodesignError::CertificateBuildError(format!("{:?}", e)))?; - - Ok(()) - } - - fn apple_extended_key_usage( - &mut self, - usage: ExtendedKeyUsagePurpose, - ) -> Result<(), AppleCodesignError> { - let payload = - bcder::encode::sequence(Oid(Bytes::copy_from_slice(usage.as_oid().as_ref())).encode()) - .to_captured(bcder::Mode::Der); - - self.add_extension_der_data( - Oid(OID_EXTENDED_KEY_USAGE.as_ref().into()), - true, - payload.as_slice(), - ); - - Ok(()) - } - - fn apple_code_signing_certificate_extension( - &mut self, - extension: CodeSigningCertificateExtension, - ) -> Result<(), AppleCodesignError> { - let (critical, payload) = match extension { - CodeSigningCertificateExtension::IPhoneDeveloper => { - // SEQUENCE (3 elem) - // OBJECT IDENTIFIER 1.2.840.113635.100.6.1.2 - // BOOLEAN true - // OCTET STRING (2 byte) 0500 - // NULL - (true, Bytes::copy_from_slice(&[0x05, 0x00])) - } - CodeSigningCertificateExtension::AppleDeveloperCertificateSubmission => { - // SEQUENCE (3 elem) - // OBJECT IDENTIFIER 1.2.840.113635.100.6.1.4 - // BOOLEAN true - // OCTET STRING (2 byte) 0500 - // NULL - (true, Bytes::copy_from_slice(&[0x05, 0x00])) - } - CodeSigningCertificateExtension::AppleMacAppSigningDevelopment => { - // SEQUENCE (3 elem) - // OBJECT IDENTIFIER 1.2.840.113635.100.6.1.7 - // BOOLEAN true - // OCTET STRING (2 byte) 0500 - // NULL - (true, Bytes::copy_from_slice(&[0x05, 0x00])) - } - CodeSigningCertificateExtension::AppleMacAppSigningSubmission => { - // SEQUENCE (3 elem) - // OBJECT IDENTIFIER 1.2.840.113635.100.6.1.8 - // BOOLEAN true - // OCTET STRING (2 byte) 0500 - // NULL - (true, Bytes::copy_from_slice(&[0x05, 0x00])) - } - CodeSigningCertificateExtension::MacDeveloper => { - // SEQUENCE (3 elem) - // OBJECT IDENTIFIER 1.2.840.113635.100.6.1.12 - // BOOLEAN true - // OCTET STRING (2 byte) 0500 - // NULL - (true, Bytes::copy_from_slice(&[0x05, 0x00])) - } - CodeSigningCertificateExtension::DeveloperIdApplication => { - // SEQUENCE (3 elem) - // OBJECT IDENTIFIER 1.2.840.113635.100.6.1.13 - // BOOLEAN true - // OCTET STRING (2 byte) 0500 - // NULL - (true, Bytes::copy_from_slice(&[0x05, 0x00])) - } - CodeSigningCertificateExtension::DeveloperIdInstaller => { - // SEQUENCE (3 elem) - // OBJECT IDENTIFIER 1.2.840.113635.100.6.1.14 - // BOOLEAN true - // OCTET STRING (2 byte) 0500 - // NULL - (true, Bytes::copy_from_slice(&[0x05, 0x00])) - } - - // The rest of these probably have the same payload. But until we see - // them, don't take chances. - _ => { - return Err(AppleCodesignError::CertificateBuildError(format!( - "don't know how to handle code signing extension {:?}", - extension - ))); - } - }; - - self.add_extension_der_data( - Oid(Bytes::copy_from_slice(extension.as_oid().as_ref())), - critical, - payload, - ); - - Ok(()) - } - - fn apple_certificate_profile( - &mut self, - profile: CertificateProfile, - ) -> Result<(), AppleCodesignError> { - // Try to keep this logic in sync with apple_guess_profile(). - match profile { - CertificateProfile::DeveloperIdApplication => { - self.constraint_not_ca(); - self.apple_extended_key_usage(ExtendedKeyUsagePurpose::CodeSigning)?; - self.key_usage(KeyUsage::DigitalSignature); - - // OID_EXTENSION_DEVELOPER_ID_DATE comes next. But we don't know what - // that should be. It is a UTF8String instead of an ASN.1 time type - // because who knows. - - self.apple_code_signing_certificate_extension( - CodeSigningCertificateExtension::DeveloperIdApplication, - )?; - } - CertificateProfile::DeveloperIdInstaller => { - self.constraint_not_ca(); - self.apple_extended_key_usage(ExtendedKeyUsagePurpose::DeveloperIdInstaller)?; - self.key_usage(KeyUsage::DigitalSignature); - - // OID_EXTENSION_DEVELOPER_ID_DATE comes next. - - self.apple_code_signing_certificate_extension( - CodeSigningCertificateExtension::DeveloperIdInstaller, - )?; - } - CertificateProfile::AppleDevelopment => { - self.constraint_not_ca(); - self.apple_extended_key_usage(ExtendedKeyUsagePurpose::CodeSigning)?; - self.key_usage(KeyUsage::DigitalSignature); - self.apple_code_signing_certificate_extension( - CodeSigningCertificateExtension::IPhoneDeveloper, - )?; - self.apple_code_signing_certificate_extension( - CodeSigningCertificateExtension::MacDeveloper, - )?; - } - CertificateProfile::AppleDistribution => { - self.constraint_not_ca(); - self.apple_extended_key_usage(ExtendedKeyUsagePurpose::CodeSigning)?; - self.key_usage(KeyUsage::DigitalSignature); - - // OID_EXTENSION_DEVELOPER_ID_DATE comes next. - - self.apple_code_signing_certificate_extension( - CodeSigningCertificateExtension::AppleMacAppSigningDevelopment, - )?; - self.apple_code_signing_certificate_extension( - CodeSigningCertificateExtension::AppleDeveloperCertificateSubmission, - )?; - } - CertificateProfile::MacInstallerDistribution => { - self.constraint_not_ca(); - self.apple_extended_key_usage( - ExtendedKeyUsagePurpose::ThirdPartyMacDeveloperInstaller, - )?; - self.key_usage(KeyUsage::DigitalSignature); - - self.apple_code_signing_certificate_extension( - CodeSigningCertificateExtension::AppleMacAppSigningSubmission, - )?; - } - } - - Ok(()) - } - - fn apple_code_signing_extensions(&self) -> Vec { - self.extensions() - .iter() - .filter_map(|ext| { - if let Ok(e) = CodeSigningCertificateExtension::try_from(&ext.id) { - Some(e) - } else { - None - } - }) - .collect::>() - } -} - -/// Create a new self-signed X.509 certificate suitable for signing code. -/// -/// The created certificate contains all the extensions needed to convey -/// that it is used for code signing and should resemble certificates. -/// -/// However, because the certificate isn't signed by Apple or another -/// trusted certificate authority, binaries signed with the certificate -/// may not pass Apple's verification requirements and the OS may refuse -/// to proceed. Needless to say, only use certificates generated with this -/// function for testing purposes only. -pub fn create_self_signed_code_signing_certificate( - algorithm: KeyAlgorithm, - profile: CertificateProfile, - team_id: &str, - person_name: &str, - country: &str, - validity_duration: chrono::Duration, -) -> Result< - ( - CapturedX509Certificate, - InMemorySigningKeyPair, - ring::pkcs8::Document, - ), - AppleCodesignError, -> { - let mut builder = X509CertificateBuilder::new(algorithm); - - builder.apple_certificate_profile(profile)?; - builder.apple_subject(team_id, person_name, country)?; - builder.validity_duration(validity_duration); - - Ok(builder.create_with_random_keypair()?) -} - -#[cfg(test)] -mod tests { - use { - super::*, - cryptographic_message_syntax::{SignedData, SignedDataBuilder, SignerBuilder}, - x509_certificate::EcdsaCurve, - }; - - #[test] - fn generate_self_signed_certificate_ecdsa() { - for curve in EcdsaCurve::all() { - create_self_signed_code_signing_certificate( - KeyAlgorithm::Ecdsa(*curve), - CertificateProfile::DeveloperIdInstaller, - "team1", - "Joe Developer", - "US", - chrono::Duration::hours(1), - ) - .unwrap(); - } - } - - #[test] - fn generate_self_signed_certificate_ed25519() { - create_self_signed_code_signing_certificate( - KeyAlgorithm::Ed25519, - CertificateProfile::DeveloperIdInstaller, - "team2", - "Joe Developer", - "US", - chrono::Duration::hours(1), - ) - .unwrap(); - } - - #[test] - fn generate_all_profiles() { - for profile in CertificateProfile::all() { - create_self_signed_code_signing_certificate( - KeyAlgorithm::Ed25519, - *profile, - "team", - "Joe Developer", - "Wakanda", - chrono::Duration::hours(1), - ) - .unwrap(); - } - } - - #[test] - fn cms_self_signed_certificate_signing_ecdsa() { - for curve in EcdsaCurve::all() { - let (cert, signing_key, _) = create_self_signed_code_signing_certificate( - KeyAlgorithm::Ecdsa(*curve), - CertificateProfile::DeveloperIdInstaller, - "team", - "Joe Developer", - "US", - chrono::Duration::hours(1), - ) - .unwrap(); - - let plaintext = "hello, world"; - - let cms = SignedDataBuilder::default() - .certificate(cert.clone()) - .content_inline(plaintext.as_bytes().to_vec()) - .signer(SignerBuilder::new(&signing_key, cert.clone())) - .build_der() - .unwrap(); - - let signed_data = SignedData::parse_ber(&cms).unwrap(); - - for signer in signed_data.signers() { - signer - .verify_signature_with_signed_data(&signed_data) - .unwrap(); - } - } - } - - #[test] - fn cms_self_signed_certificate_signing_ed25519() { - let (cert, signing_key, _) = create_self_signed_code_signing_certificate( - KeyAlgorithm::Ed25519, - CertificateProfile::DeveloperIdInstaller, - "team", - "Joe Developer", - "US", - chrono::Duration::hours(1), - ) - .unwrap(); - - let plaintext = "hello, world"; - - let cms = SignedDataBuilder::default() - .certificate(cert.clone()) - .content_inline(plaintext.as_bytes().to_vec()) - .signer(SignerBuilder::new(&signing_key, cert)) - .build_der() - .unwrap(); - - let signed_data = SignedData::parse_ber(&cms).unwrap(); - - for signer in signed_data.signers() { - signer - .verify_signature_with_signed_data(&signed_data) - .unwrap(); - } - } - - #[test] - fn third_mac_mac() { - let der = include_bytes!("testdata/apple-signed-3rd-party-mac.cer"); - let cert = CapturedX509Certificate::from_der(der.to_vec()).unwrap(); - - assert_eq!( - cert.apple_extended_key_usage_purposes(), - vec![ExtendedKeyUsagePurpose::ThirdPartyMacDeveloperInstaller] - ); - assert_eq!( - cert.apple_code_signing_extensions(), - vec![CodeSigningCertificateExtension::AppleMacAppSigningSubmission] - ); - assert_eq!( - cert.apple_guess_profile(), - Some(CertificateProfile::MacInstallerDistribution) - ); - assert_eq!( - cert.apple_issuing_chain(), - vec![ - KnownCertificate::WwdrG3, - KnownCertificate::AppleRootCa, - KnownCertificate::AppleComputerIncRoot - ] - ); - assert!(cert.chains_to_apple_root_ca()); - assert_eq!( - cert.apple_root_certificate_chain(), - Some(vec![ - cert.clone(), - (*KnownCertificate::WwdrG3).clone(), - (*KnownCertificate::AppleRootCa).clone() - ]) - ); - assert_eq!(cert.apple_team_id(), Some("MK22MZP987".into())); - - let mut builder = X509CertificateBuilder::new(KeyAlgorithm::Ecdsa(EcdsaCurve::Secp256r1)); - builder - .apple_certificate_profile(CertificateProfile::MacInstallerDistribution) - .unwrap(); - - let built = builder.create_with_random_keypair().unwrap().0; - - assert_eq!( - built.apple_extended_key_usage_purposes(), - cert.apple_extended_key_usage_purposes() - ); - assert_eq!( - built.apple_code_signing_extensions(), - cert.apple_code_signing_extensions() - ); - assert_eq!(built.apple_guess_profile(), cert.apple_guess_profile()); - assert_eq!(built.apple_issuing_chain(), vec![]); - assert!(!built.chains_to_apple_root_ca()); - assert!(built.apple_root_certificate_chain().is_none()); - } - - #[test] - fn apple_development() { - let der = include_bytes!("testdata/apple-signed-apple-development.cer"); - let cert = CapturedX509Certificate::from_der(der.to_vec()).unwrap(); - - assert_eq!( - cert.apple_extended_key_usage_purposes(), - vec![ExtendedKeyUsagePurpose::CodeSigning] - ); - assert_eq!( - cert.apple_code_signing_extensions(), - vec![ - CodeSigningCertificateExtension::IPhoneDeveloper, - CodeSigningCertificateExtension::MacDeveloper - ] - ); - assert_eq!( - cert.apple_guess_profile(), - Some(CertificateProfile::AppleDevelopment) - ); - assert_eq!( - cert.apple_issuing_chain(), - vec![ - KnownCertificate::WwdrG3, - KnownCertificate::AppleRootCa, - KnownCertificate::AppleComputerIncRoot - ], - ); - assert!(cert.chains_to_apple_root_ca()); - assert_eq!( - cert.apple_root_certificate_chain(), - Some(vec![ - cert.clone(), - (*KnownCertificate::WwdrG3).clone(), - (*KnownCertificate::AppleRootCa).clone() - ]) - ); - assert_eq!(cert.apple_team_id(), Some("MK22MZP987".into())); - - let mut builder = X509CertificateBuilder::new(KeyAlgorithm::Ecdsa(EcdsaCurve::Secp256r1)); - builder - .apple_certificate_profile(CertificateProfile::AppleDevelopment) - .unwrap(); - - let built = builder.create_with_random_keypair().unwrap().0; - - assert_eq!( - built.apple_extended_key_usage_purposes(), - cert.apple_extended_key_usage_purposes() - ); - assert_eq!( - built.apple_code_signing_extensions(), - cert.apple_code_signing_extensions() - ); - assert_eq!(built.apple_guess_profile(), cert.apple_guess_profile()); - assert_eq!(built.apple_issuing_chain(), vec![]); - assert!(!built.chains_to_apple_root_ca()); - assert!(built.apple_root_certificate_chain().is_none()); - } - - #[test] - fn apple_distribution() { - let der = include_bytes!("testdata/apple-signed-apple-distribution.cer"); - let cert = CapturedX509Certificate::from_der(der.to_vec()).unwrap(); - - assert_eq!( - cert.apple_extended_key_usage_purposes(), - vec![ExtendedKeyUsagePurpose::CodeSigning] - ); - assert_eq!( - cert.apple_code_signing_extensions(), - vec![ - CodeSigningCertificateExtension::AppleMacAppSigningDevelopment, - CodeSigningCertificateExtension::AppleDeveloperCertificateSubmission - ] - ); - assert_eq!( - cert.apple_guess_profile(), - Some(CertificateProfile::AppleDistribution) - ); - assert_eq!( - cert.apple_issuing_chain(), - vec![ - KnownCertificate::WwdrG3, - KnownCertificate::AppleRootCa, - KnownCertificate::AppleComputerIncRoot - ], - ); - assert!(cert.chains_to_apple_root_ca()); - assert_eq!( - cert.apple_root_certificate_chain(), - Some(vec![ - cert.clone(), - (*KnownCertificate::WwdrG3).clone(), - (*KnownCertificate::AppleRootCa).clone() - ]) - ); - assert_eq!(cert.apple_team_id(), Some("MK22MZP987".into())); - - let mut builder = X509CertificateBuilder::new(KeyAlgorithm::Ecdsa(EcdsaCurve::Secp256r1)); - builder - .apple_certificate_profile(CertificateProfile::AppleDistribution) - .unwrap(); - - let built = builder.create_with_random_keypair().unwrap().0; - - assert_eq!( - built.apple_extended_key_usage_purposes(), - cert.apple_extended_key_usage_purposes() - ); - assert_eq!( - built.apple_code_signing_extensions(), - cert.apple_code_signing_extensions() - ); - assert_eq!(built.apple_guess_profile(), cert.apple_guess_profile()); - assert_eq!(built.apple_issuing_chain(), vec![]); - assert!(!built.chains_to_apple_root_ca()); - assert!(built.apple_root_certificate_chain().is_none()); - } - - #[test] - fn apple_developer_id_application() { - let der = include_bytes!("testdata/apple-signed-developer-id-application.cer"); - let cert = CapturedX509Certificate::from_der(der.to_vec()).unwrap(); - - assert_eq!( - cert.apple_extended_key_usage_purposes(), - vec![ExtendedKeyUsagePurpose::CodeSigning] - ); - assert_eq!( - cert.apple_code_signing_extensions(), - vec![ - CodeSigningCertificateExtension::DeveloperIdDate, - CodeSigningCertificateExtension::DeveloperIdApplication - ] - ); - assert_eq!( - cert.apple_guess_profile(), - Some(CertificateProfile::DeveloperIdApplication) - ); - assert_eq!( - cert.apple_issuing_chain(), - vec![ - KnownCertificate::DeveloperIdG1, - KnownCertificate::AppleRootCa, - KnownCertificate::AppleComputerIncRoot - ] - ); - assert!(cert.chains_to_apple_root_ca()); - assert_eq!( - cert.apple_root_certificate_chain(), - Some(vec![ - cert.clone(), - (*KnownCertificate::DeveloperIdG1).clone(), - (*KnownCertificate::AppleRootCa).clone() - ]) - ); - assert_eq!(cert.apple_team_id(), Some("MK22MZP987".into())); - - let mut builder = X509CertificateBuilder::new(KeyAlgorithm::Ecdsa(EcdsaCurve::Secp256r1)); - builder - .apple_certificate_profile(CertificateProfile::DeveloperIdApplication) - .unwrap(); - - let built = builder.create_with_random_keypair().unwrap().0; - - assert_eq!( - built.apple_extended_key_usage_purposes(), - cert.apple_extended_key_usage_purposes() - ); - assert_eq!( - built.apple_code_signing_extensions(), - // We don't write out the date extension. - cert.apple_code_signing_extensions() - .into_iter() - .filter(|e| !matches!(e, CodeSigningCertificateExtension::DeveloperIdDate)) - .collect::>() - ); - assert_eq!(built.apple_guess_profile(), cert.apple_guess_profile()); - assert_eq!(built.apple_issuing_chain(), vec![]); - assert!(!built.chains_to_apple_root_ca()); - assert!(built.apple_root_certificate_chain().is_none()); - } - - #[test] - fn apple_developer_id_installer() { - let der = include_bytes!("testdata/apple-signed-developer-id-installer.cer"); - let cert = CapturedX509Certificate::from_der(der.to_vec()).unwrap(); - - assert_eq!( - cert.apple_extended_key_usage_purposes(), - vec![ExtendedKeyUsagePurpose::DeveloperIdInstaller] - ); - assert_eq!( - cert.apple_code_signing_extensions(), - vec![ - CodeSigningCertificateExtension::DeveloperIdDate, - CodeSigningCertificateExtension::DeveloperIdInstaller - ] - ); - assert_eq!( - cert.apple_guess_profile(), - Some(CertificateProfile::DeveloperIdInstaller) - ); - assert_eq!( - cert.apple_issuing_chain(), - vec![ - KnownCertificate::DeveloperIdG1, - KnownCertificate::AppleRootCa, - KnownCertificate::AppleComputerIncRoot - ] - ); - assert!(cert.chains_to_apple_root_ca()); - assert_eq!( - cert.apple_root_certificate_chain(), - Some(vec![ - cert.clone(), - (*KnownCertificate::DeveloperIdG1).clone(), - (*KnownCertificate::AppleRootCa).clone() - ]) - ); - assert_eq!(cert.apple_team_id(), Some("MK22MZP987".into())); - - let mut builder = X509CertificateBuilder::new(KeyAlgorithm::Ecdsa(EcdsaCurve::Secp256r1)); - builder - .apple_certificate_profile(CertificateProfile::DeveloperIdInstaller) - .unwrap(); - - let built = builder.create_with_random_keypair().unwrap().0; - - assert_eq!( - built.apple_extended_key_usage_purposes(), - cert.apple_extended_key_usage_purposes() - ); - assert_eq!( - built.apple_code_signing_extensions(), - // We don't write out the date extension. - cert.apple_code_signing_extensions() - .into_iter() - .filter(|e| !matches!(e, CodeSigningCertificateExtension::DeveloperIdDate)) - .collect::>() - ); - assert_eq!(built.apple_guess_profile(), cert.apple_guess_profile()); - assert_eq!(built.apple_issuing_chain(), vec![]); - assert!(!built.chains_to_apple_root_ca()); - assert!(built.apple_root_certificate_chain().is_none()); - } -} diff --git a/apple-codesign/src/code_directory.rs b/apple-codesign/src/code_directory.rs deleted file mode 100644 index 7441ef22c..000000000 --- a/apple-codesign/src/code_directory.rs +++ /dev/null @@ -1,800 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Code directory data structure and related types. - -use { - crate::{ - embedded_signature::{ - read_and_validate_blob_header, Blob, CodeSigningMagic, CodeSigningSlot, Digest, - DigestType, - }, - error::AppleCodesignError, - macho::{MachoTarget, Platform}, - }, - scroll::{IOwrite, Pread}, - semver::Version, - std::{borrow::Cow, collections::HashMap, io::Write, str::FromStr}, -}; - -bitflags::bitflags! { - /// Code signature flags. - /// - /// These flags are embedded in the Code Directory and govern use of the embedded - /// signature. - #[derive(Default)] - pub struct CodeSignatureFlags: u32 { - /// Code may act as a host that controls and supervises guest code. - const HOST = 0x0001; - /// The code has been sealed without a signing identity. - const ADHOC = 0x0002; - /// Set the "hard" status bit for the code when it starts running. - const FORCE_HARD = 0x0100; - /// Implicitly set the "kill" status bit for the code when it starts running. - const FORCE_KILL = 0x0200; - /// Force certificate expiration checks. - const FORCE_EXPIRATION = 0x0400; - /// Restrict dyld loading. - const RESTRICT = 0x0800; - /// Enforce code signing. - const ENFORCEMENT = 0x1000; - /// Library validation required. - const LIBRARY_VALIDATION = 0x2000; - /// Apply runtime hardening policies. - const RUNTIME = 0x10000; - /// The code was automatically signed by the linker. - /// - /// This signature should be ignored in any new signing operation. - const LINKER_SIGNED = 0x20000; - } -} - -impl FromStr for CodeSignatureFlags { - type Err = AppleCodesignError; - - fn from_str(s: &str) -> Result { - match s { - "host" => Ok(Self::HOST), - "hard" => Ok(Self::FORCE_HARD), - "kill" => Ok(Self::FORCE_KILL), - "expires" => Ok(Self::FORCE_EXPIRATION), - "library" => Ok(Self::LIBRARY_VALIDATION), - "runtime" => Ok(Self::RUNTIME), - "linker-signed" => Ok(Self::LINKER_SIGNED), - _ => Err(AppleCodesignError::CodeSignatureUnknownFlag(s.to_string())), - } - } -} - -impl CodeSignatureFlags { - /// Obtain all flags that can be set by the user. - /// - /// Maps to variants that have a `from_str()` implementation. - pub fn all_user_configurable() -> Vec<&'static str> { - vec![ - "host", - "hard", - "kill", - "expires", - "library", - "runtime", - "linker-signed", - ] - } - - /// Attempt to convert a series of strings into a [CodeSignatureFlags]. - pub fn from_strs(s: &[&str]) -> Result { - let mut flags = CodeSignatureFlags::empty(); - - for s in s { - flags |= Self::from_str(s)?; - } - - Ok(flags) - } -} - -bitflags::bitflags! { - /// Flags that influence behavior of executable segment. - #[derive(Default)] - pub struct ExecutableSegmentFlags: u64 { - /// Executable segment belongs to main binary. - const MAIN_BINARY = 0x0001; - /// Allow unsigned pages (for debugging). - const ALLOW_UNSIGNED = 0x0010; - /// Main binary is debugger. - const DEBUGGER = 0x0020; - /// JIT enabled. - const JIT = 0x0040; - /// Skip library validation (obsolete). - const SKIP_LIBRARY_VALIDATION = 0x0080; - /// Can bless code directory hash for execution. - const CAN_LOAD_CD_HASH = 0x0100; - /// Can execute blessed code directory hash. - const CAN_EXEC_CD_HASH = 0x0200; - } -} - -impl FromStr for ExecutableSegmentFlags { - type Err = AppleCodesignError; - - fn from_str(s: &str) -> Result { - match s { - "main-binary" => Ok(Self::MAIN_BINARY), - "allow-unsigned" => Ok(Self::ALLOW_UNSIGNED), - "debugger" => Ok(Self::DEBUGGER), - "jit" => Ok(Self::JIT), - "skip-library-validation" => Ok(Self::SKIP_LIBRARY_VALIDATION), - "can-load-cd-hash" => Ok(Self::CAN_LOAD_CD_HASH), - "can-exec-cd-hash" => Ok(Self::CAN_EXEC_CD_HASH), - _ => Err(AppleCodesignError::ExecutableSegmentUnknownFlag( - s.to_string(), - )), - } - } -} - -/// Version of Code Directory data structure. -#[derive(Clone, Copy, Debug, PartialEq)] -#[repr(u32)] -pub enum CodeDirectoryVersion { - Initial = 0x20000, - SupportsScatter = 0x20100, - SupportsTeamId = 0x20200, - SupportsCodeLimit64 = 0x20300, - SupportsExecutableSegment = 0x20400, - SupportsRuntime = 0x20500, - SupportsLinkage = 0x20600, -} - -#[repr(C)] -pub struct Scatter { - /// Number of pages. 0 for sentinel only. - count: u32, - /// First page number. - base: u32, - /// Offset in target. - target_offset: u64, - /// Reserved. - spare: u64, -} - -fn get_hashes(data: &[u8], offset: usize, count: usize, hash_size: usize) -> Vec> { - data[offset..offset + (count * hash_size)] - .chunks(hash_size) - .map(|data| Digest { data: data.into() }) - .collect() -} - -/// Represents a code directory blob entry. -/// -/// This struct is versioned and has been extended over time. -/// -/// The struct here represents a superset of all fields in all versions. -/// -/// The parser will set `Option` fields to `None` for instances -/// where the version is lower than the version that field was introduced in. -#[derive(Debug, Default)] -pub struct CodeDirectoryBlob<'a> { - /// Compatibility version. - pub version: u32, - /// Setup and mode flags. - pub flags: CodeSignatureFlags, - // digest_offset, ident_offset, n_special_slots, and n_code_slots not stored - // explicitly because they are redundant with derived fields. - /// Limit to main image signature range. - /// - /// This is the file-level offset to stop digesting code data at. - /// It likely corresponds to the file-offset offset where the - /// embedded signature data starts in the `__LINKEDIT` segment. - pub code_limit: u32, - /// Size of each slot/code digest in bytes. - pub digest_size: u8, - /// Type of content digest being used. - pub digest_type: DigestType, - /// Platform identifier. 0 if not platform binary. - pub platform: u8, - /// Page size in bytes. (stored as log u8) - pub page_size: u32, - /// Unused (must be 0). - pub spare2: u32, - // Version 0x20100 - /// Offset of optional scatter vector. - pub scatter_offset: Option, - // Version 0x20200 - // team_offset not stored because it is redundant with derived stored str. - // Version 0x20300 - /// Unused (must be 0). - pub spare3: Option, - /// Limit to main image signature range, 64 bits. - pub code_limit_64: Option, - // Version 0x20400 - /// Offset of executable segment. - pub exec_seg_base: Option, - /// Limit of executable segment. - pub exec_seg_limit: Option, - /// Executable segment flags. - pub exec_seg_flags: Option, - // Version 0x20500 - pub runtime: Option, - pub pre_encrypt_offset: Option, - // Version 0x20600 - pub linkage_hash_type: Option, - pub linkage_truncated: Option, - pub spare4: Option, - pub linkage_offset: Option, - pub linkage_size: Option, - - // End of blob header data / start of derived data. - pub ident: Cow<'a, str>, - pub team_name: Option>, - pub code_digests: Vec>, - pub special_digests: HashMap>, -} - -impl<'a> Blob<'a> for CodeDirectoryBlob<'a> { - fn magic() -> u32 { - u32::from(CodeSigningMagic::CodeDirectory) - } - - fn from_blob_bytes(data: &'a [u8]) -> Result { - read_and_validate_blob_header(data, Self::magic(), "code directory blob")?; - - let offset = &mut 8; - - let version = data.gread_with(offset, scroll::BE)?; - let flags = data.gread_with::(offset, scroll::BE)?; - let flags = unsafe { CodeSignatureFlags::from_bits_unchecked(flags) }; - assert_eq!(*offset, 0x10); - let digest_offset = data.gread_with::(offset, scroll::BE)?; - let ident_offset = data.gread_with::(offset, scroll::BE)?; - let n_special_slots = data.gread_with::(offset, scroll::BE)?; - let n_code_slots = data.gread_with::(offset, scroll::BE)?; - assert_eq!(*offset, 0x20); - let code_limit = data.gread_with(offset, scroll::BE)?; - let digest_size = data.gread_with(offset, scroll::BE)?; - let digest_type = data.gread_with::(offset, scroll::BE)?.into(); - let platform = data.gread_with(offset, scroll::BE)?; - let page_size = data.gread_with::(offset, scroll::BE)?; - let page_size = 2u32.pow(page_size as u32); - let spare2 = data.gread_with(offset, scroll::BE)?; - - let scatter_offset = if version >= CodeDirectoryVersion::SupportsScatter as u32 { - let v = data.gread_with(offset, scroll::BE)?; - - if v != 0 { - Some(v) - } else { - None - } - } else { - None - }; - let team_offset = if version >= CodeDirectoryVersion::SupportsTeamId as u32 { - assert_eq!(*offset, 0x30); - let v = data.gread_with::(offset, scroll::BE)?; - - if v != 0 { - Some(v) - } else { - None - } - } else { - None - }; - - let (spare3, code_limit_64) = if version >= CodeDirectoryVersion::SupportsCodeLimit64 as u32 - { - ( - Some(data.gread_with(offset, scroll::BE)?), - Some(data.gread_with(offset, scroll::BE)?), - ) - } else { - (None, None) - }; - - let (exec_seg_base, exec_seg_limit, exec_seg_flags) = - if version >= CodeDirectoryVersion::SupportsExecutableSegment as u32 { - assert_eq!(*offset, 0x40); - ( - Some(data.gread_with(offset, scroll::BE)?), - Some(data.gread_with(offset, scroll::BE)?), - Some(data.gread_with::(offset, scroll::BE)?), - ) - } else { - (None, None, None) - }; - - let exec_seg_flags = exec_seg_flags - .map(|flags| unsafe { ExecutableSegmentFlags::from_bits_unchecked(flags) }); - - let (runtime, pre_encrypt_offset) = - if version >= CodeDirectoryVersion::SupportsRuntime as u32 { - assert_eq!(*offset, 0x58); - ( - Some(data.gread_with(offset, scroll::BE)?), - Some(data.gread_with(offset, scroll::BE)?), - ) - } else { - (None, None) - }; - - let (linkage_hash_type, linkage_truncated, spare4, linkage_offset, linkage_size) = - if version >= CodeDirectoryVersion::SupportsLinkage as u32 { - assert_eq!(*offset, 0x60); - ( - Some(data.gread_with(offset, scroll::BE)?), - Some(data.gread_with(offset, scroll::BE)?), - Some(data.gread_with(offset, scroll::BE)?), - Some(data.gread_with(offset, scroll::BE)?), - Some(data.gread_with(offset, scroll::BE)?), - ) - } else { - (None, None, None, None, None) - }; - - // Find trailing null in identifier string. - let ident = match data[ident_offset as usize..] - .split(|&b| b == 0) - .map(std::str::from_utf8) - .next() - { - Some(res) => { - Cow::from(res.map_err(|_| AppleCodesignError::CodeDirectoryMalformedIdentifier)?) - } - None => { - return Err(AppleCodesignError::CodeDirectoryMalformedIdentifier); - } - }; - - let team_name = if let Some(team_offset) = team_offset { - match data[team_offset as usize..] - .split(|&b| b == 0) - .map(std::str::from_utf8) - .next() - { - Some(res) => { - Some(Cow::from(res.map_err(|_| { - AppleCodesignError::CodeDirectoryMalformedTeam - })?)) - } - None => { - return Err(AppleCodesignError::CodeDirectoryMalformedTeam); - } - } - } else { - None - }; - - let code_digests = get_hashes( - data, - digest_offset as usize, - n_code_slots as usize, - digest_size as usize, - ); - - let special_digests = get_hashes( - data, - (digest_offset - (digest_size as u32 * n_special_slots)) as usize, - n_special_slots as usize, - digest_size as usize, - ) - .into_iter() - .enumerate() - .map(|(i, h)| (CodeSigningSlot::from(n_special_slots - i as u32), h)) - .collect(); - - Ok(Self { - version, - flags, - code_limit, - digest_size, - digest_type, - platform, - page_size, - spare2, - scatter_offset, - spare3, - code_limit_64, - exec_seg_base, - exec_seg_limit, - exec_seg_flags, - runtime, - pre_encrypt_offset, - linkage_hash_type, - linkage_truncated, - spare4, - linkage_offset, - linkage_size, - ident, - team_name, - code_digests, - special_digests, - }) - } - - fn serialize_payload(&self) -> Result, AppleCodesignError> { - let mut cursor = std::io::Cursor::new(Vec::::new()); - - // We need to do this in 2 phases because we don't know the length until - // we build up the data structure. - - cursor.iowrite_with(self.version, scroll::BE)?; - cursor.iowrite_with(self.flags.bits, scroll::BE)?; - let digest_offset_cursor_position = cursor.position(); - cursor.iowrite_with(0u32, scroll::BE)?; - let ident_offset_cursor_position = cursor.position(); - cursor.iowrite_with(0u32, scroll::BE)?; - assert_eq!(cursor.position(), 0x10); - - // Digest offsets and counts are wonky. The recorded digest offset is the beginning - // of code digests and special digests are in "negative" indices before - // that offset. Digests are also at the index of their CodeSigningSlot constant. - // e.g. Code Directory is the first element in the specials array because - // it is slot 0. This means we need to write out empty digests for missing - // special slots. Our local specials HashMap may not have all entries. So compute - // how many specials there should be and write that here. We'll insert placeholder - // digests later. - let highest_slot = self - .special_digests - .keys() - .map(|slot| u32::from(*slot)) - .max() - .unwrap_or(0); - - cursor.iowrite_with(highest_slot as u32, scroll::BE)?; - cursor.iowrite_with(self.code_digests.len() as u32, scroll::BE)?; - cursor.iowrite_with(self.code_limit, scroll::BE)?; - cursor.iowrite_with(self.digest_size, scroll::BE)?; - cursor.iowrite_with(u8::from(self.digest_type), scroll::BE)?; - cursor.iowrite_with(self.platform, scroll::BE)?; - cursor.iowrite_with(self.page_size.trailing_zeros() as u8, scroll::BE)?; - assert_eq!(cursor.position(), 0x20); - cursor.iowrite_with(self.spare2, scroll::BE)?; - - let mut scatter_offset_cursor_position = None; - let mut team_offset_cursor_position = None; - - if self.version >= CodeDirectoryVersion::SupportsScatter as u32 { - scatter_offset_cursor_position = Some(cursor.position()); - cursor.iowrite_with(self.scatter_offset.unwrap_or(0), scroll::BE)?; - - if self.version >= CodeDirectoryVersion::SupportsTeamId as u32 { - team_offset_cursor_position = Some(cursor.position()); - cursor.iowrite_with(0u32, scroll::BE)?; - - if self.version >= CodeDirectoryVersion::SupportsCodeLimit64 as u32 { - cursor.iowrite_with(self.spare3.unwrap_or(0), scroll::BE)?; - assert_eq!(cursor.position(), 0x30); - cursor.iowrite_with(self.code_limit_64.unwrap_or(0), scroll::BE)?; - - if self.version >= CodeDirectoryVersion::SupportsExecutableSegment as u32 { - cursor.iowrite_with(self.exec_seg_base.unwrap_or(0), scroll::BE)?; - assert_eq!(cursor.position(), 0x40); - cursor.iowrite_with(self.exec_seg_limit.unwrap_or(0), scroll::BE)?; - cursor.iowrite_with( - self.exec_seg_flags - .unwrap_or_else(ExecutableSegmentFlags::empty) - .bits, - scroll::BE, - )?; - - if self.version >= CodeDirectoryVersion::SupportsRuntime as u32 { - assert_eq!(cursor.position(), 0x50); - cursor.iowrite_with(self.runtime.unwrap_or(0), scroll::BE)?; - cursor - .iowrite_with(self.pre_encrypt_offset.unwrap_or(0), scroll::BE)?; - - if self.version >= CodeDirectoryVersion::SupportsLinkage as u32 { - cursor.iowrite_with( - self.linkage_hash_type.unwrap_or(0), - scroll::BE, - )?; - cursor.iowrite_with( - self.linkage_truncated.unwrap_or(0), - scroll::BE, - )?; - cursor.iowrite_with(self.spare4.unwrap_or(0), scroll::BE)?; - cursor - .iowrite_with(self.linkage_offset.unwrap_or(0), scroll::BE)?; - assert_eq!(cursor.position(), 0x60); - cursor.iowrite_with(self.linkage_size.unwrap_or(0), scroll::BE)?; - } - } - } - } - } - } - - // We've written all the struct fields. Now write variable length fields. - - let identity_offset = cursor.position(); - cursor.write_all(self.ident.as_bytes())?; - cursor.write_all(b"\0")?; - - let team_offset = cursor.position(); - if team_offset_cursor_position.is_some() { - if let Some(team_name) = &self.team_name { - cursor.write_all(team_name.as_bytes())?; - cursor.write_all(b"\0")?; - } - } - - // TODO consider aligning cursor on page boundary here for performance? - - // The boundary conditions are a bit wonky here. We want to go from greatest - // to smallest, not writing index 0 because that's the first code digest. - for slot_index in (1..highest_slot + 1).rev() { - let slot = CodeSigningSlot::from(slot_index); - assert!( - slot.is_code_directory_specials_expressible(), - "slot is expressible in code directory special digests" - ); - - if let Some(digest) = self.special_digests.get(&slot) { - assert_eq!( - digest.data.len(), - self.digest_size as usize, - "special slot digest length matches expected length" - ); - cursor.write_all(&digest.data)?; - } else { - cursor.write_all(&b"\0".repeat(self.digest_size as usize))?; - } - } - - let code_digests_start_offset = cursor.position(); - - for digest in &self.code_digests { - cursor.write_all(&digest.data)?; - } - - // TODO write out scatter vector. - - // Now go back and update the placeholder offsets. We need to add 8 to account - // for the blob header, which isn't present in this buffer. - cursor.set_position(digest_offset_cursor_position); - cursor.iowrite_with(code_digests_start_offset as u32 + 8, scroll::BE)?; - - cursor.set_position(ident_offset_cursor_position); - cursor.iowrite_with(identity_offset as u32 + 8, scroll::BE)?; - - if scatter_offset_cursor_position.is_some() && self.scatter_offset.is_some() { - return Err(AppleCodesignError::Unimplemented("scatter offset")); - } - - if let Some(offset) = team_offset_cursor_position { - if self.team_name.is_some() { - cursor.set_position(offset); - cursor.iowrite_with(team_offset as u32 + 8, scroll::BE)?; - } - } - - Ok(cursor.into_inner()) - } -} - -impl<'a> CodeDirectoryBlob<'a> { - /// Obtain the mapping of slots to digests. - pub fn slot_digests(&self) -> &HashMap> { - &self.special_digests - } - - /// Obtain the recorded digest for a given [CodeSigningSlot]. - pub fn slot_digest(&self, slot: CodeSigningSlot) -> Option<&Digest<'a>> { - self.special_digests.get(&slot) - } - - /// Set the digest for a given slot. - pub fn set_slot_digest( - &mut self, - slot: CodeSigningSlot, - digest: impl Into>, - ) -> Result<(), AppleCodesignError> { - if !slot.is_code_directory_specials_expressible() { - return Err(AppleCodesignError::LogicError(format!( - "slot {:?} cannot have its digest expressed on code directories", - slot - ))); - } - - let digest = digest.into(); - - if digest.data.len() != self.digest_size as usize { - return Err(AppleCodesignError::LogicError(format!( - "attempt to assign digest for slot {:?} whose length {} does not match code directory digest length {}", - slot, digest.data.len(), self.digest_size - - ))); - } - - self.special_digests.insert(slot, digest); - - Ok(()) - } - - /// Adjust the version of the data structure according to what fields are set. - /// - /// Returns the old version. - pub fn adjust_version(&mut self, target: Option) -> u32 { - let old_version = self.version; - - let mut minimum_version = CodeDirectoryVersion::Initial; - - if self.scatter_offset.is_some() { - minimum_version = CodeDirectoryVersion::SupportsScatter; - } - if self.team_name.is_some() { - minimum_version = CodeDirectoryVersion::SupportsTeamId; - } - if self.spare3.is_some() || self.code_limit_64.is_some() { - minimum_version = CodeDirectoryVersion::SupportsCodeLimit64; - } - if self.exec_seg_base.is_some() - || self.exec_seg_limit.is_some() - || self.exec_seg_flags.is_some() - { - minimum_version = CodeDirectoryVersion::SupportsExecutableSegment; - } - if self.runtime.is_some() || self.pre_encrypt_offset.is_some() { - minimum_version = CodeDirectoryVersion::SupportsRuntime; - } - if self.linkage_hash_type.is_some() - || self.linkage_truncated.is_some() - || self.spare4.is_some() - || self.linkage_offset.is_some() - || self.linkage_size.is_some() - { - minimum_version = CodeDirectoryVersion::SupportsLinkage; - } - - // Some platforms have hard requirements for the minimum version. If - // targeting settings are in effect, we raise the minimum version accordingly. - if let Some(target) = target { - let target_minimum = match target.platform { - // iOS >= 15 requires a modern code signature format. - Platform::IOs | Platform::IosSimulator => { - if target.minimum_os_version >= Version::new(15, 0, 0) { - CodeDirectoryVersion::SupportsExecutableSegment - } else { - CodeDirectoryVersion::Initial - } - } - // Let's bump the minimum version for macOS 12 out of principle. - Platform::MacOs => { - if target.minimum_os_version >= Version::new(12, 0, 0) { - CodeDirectoryVersion::SupportsExecutableSegment - } else { - CodeDirectoryVersion::Initial - } - } - _ => CodeDirectoryVersion::Initial, - }; - - if target_minimum as u32 > minimum_version as u32 { - minimum_version = target_minimum; - } - } - - self.version = minimum_version as u32; - - old_version - } - - /// Clears optional fields that are newer than the current version. - /// - /// The C structure is versioned and our Rust struct is a superset of - /// all versions. While our serializer should omit too new fields for - /// a given version, it is possible for some optional fields to be set - /// when they wouldn't get serialized. - /// - /// Calling this function will set fields not present in the current - /// version to None. - pub fn clear_newer_fields(&mut self) { - if self.version < CodeDirectoryVersion::SupportsScatter as u32 { - self.scatter_offset = None; - } - if self.version < CodeDirectoryVersion::SupportsTeamId as u32 { - self.team_name = None; - } - if self.version < CodeDirectoryVersion::SupportsCodeLimit64 as u32 { - self.spare3 = None; - self.code_limit_64 = None; - } - if self.version < CodeDirectoryVersion::SupportsExecutableSegment as u32 { - self.exec_seg_base = None; - self.exec_seg_limit = None; - self.exec_seg_flags = None; - } - if self.version < CodeDirectoryVersion::SupportsRuntime as u32 { - self.runtime = None; - self.pre_encrypt_offset = None; - } - if self.version < CodeDirectoryVersion::SupportsLinkage as u32 { - self.linkage_hash_type = None; - self.linkage_truncated = None; - self.spare4 = None; - self.linkage_offset = None; - self.linkage_size = None; - } - } - - pub fn to_owned(&self) -> CodeDirectoryBlob<'static> { - CodeDirectoryBlob { - version: self.version, - flags: self.flags, - code_limit: self.code_limit, - digest_size: self.digest_size, - digest_type: self.digest_type, - platform: self.platform, - page_size: self.page_size, - spare2: self.spare2, - scatter_offset: self.scatter_offset, - spare3: self.spare3, - code_limit_64: self.code_limit_64, - exec_seg_base: self.exec_seg_base, - exec_seg_limit: self.exec_seg_limit, - exec_seg_flags: self.exec_seg_flags, - runtime: self.runtime, - pre_encrypt_offset: self.pre_encrypt_offset, - linkage_hash_type: self.linkage_hash_type, - linkage_truncated: self.linkage_truncated, - spare4: self.spare4, - linkage_offset: self.linkage_offset, - linkage_size: self.linkage_size, - ident: Cow::Owned(self.ident.clone().into_owned()), - team_name: self - .team_name - .as_ref() - .map(|x| Cow::Owned(x.clone().into_owned())), - code_digests: self - .code_digests - .iter() - .map(|h| h.to_owned()) - .collect::>(), - special_digests: self - .special_digests - .iter() - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .collect::>(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn code_signature_flags_from_str() { - assert_eq!( - CodeSignatureFlags::from_str("host").unwrap(), - CodeSignatureFlags::HOST - ); - assert_eq!( - CodeSignatureFlags::from_str("hard").unwrap(), - CodeSignatureFlags::FORCE_HARD - ); - assert_eq!( - CodeSignatureFlags::from_str("kill").unwrap(), - CodeSignatureFlags::FORCE_KILL - ); - assert_eq!( - CodeSignatureFlags::from_str("expires").unwrap(), - CodeSignatureFlags::FORCE_EXPIRATION - ); - assert_eq!( - CodeSignatureFlags::from_str("library").unwrap(), - CodeSignatureFlags::LIBRARY_VALIDATION - ); - assert_eq!( - CodeSignatureFlags::from_str("runtime").unwrap(), - CodeSignatureFlags::RUNTIME - ); - assert_eq!( - CodeSignatureFlags::from_str("linker-signed").unwrap(), - CodeSignatureFlags::LINKER_SIGNED - ); - } -} diff --git a/apple-codesign/src/code_requirement.rs b/apple-codesign/src/code_requirement.rs deleted file mode 100644 index 231281728..000000000 --- a/apple-codesign/src/code_requirement.rs +++ /dev/null @@ -1,2015 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -/*! Code requirement language primitives. - -Code signatures contain a binary encoded expression tree denoting requirements. -There is a human friendly DSL that can be turned into these binary expressions -using the `csreq` Apple tool. This module reimplements that language. - -# Binary Encoding - -Requirement expressions consist of opcodes. An opcode is defined by a u32 where -the high byte contains flags and the lower 3 bytes denote the opcode value. - -Some opcodes have payloads and the payload varies by opcode. A common pattern -is to length encode arbitrary data via a u32 denoting the length and N bytes -to follow. - -String data is not guaranteed to be terminated by a NULL. However, variable -length data is padded will NULL bytes so the next opcode is always aligned -on 4 byte boundaries. - -*/ - -use { - crate::{ - embedded_signature::{ - read_and_validate_blob_header, CodeSigningMagic, RequirementBlob, RequirementSetBlob, - }, - error::AppleCodesignError, - }, - bcder::Oid, - chrono::TimeZone, - scroll::{IOwrite, Pread}, - std::{ - borrow::Cow, - cmp::Ordering, - fmt::{Debug, Display}, - io::Write, - ops::{Deref, DerefMut}, - }, -}; - -const OPCODE_FLAG_MASK: u32 = 0xff000000; -const OPCODE_VALUE_MASK: u32 = 0x00ffffff; - -/// Opcode flag meaning has size field, okay to default to false. -#[allow(unused)] -const OPCODE_FLAG_DEFAULT_FALSE: u32 = 0x80000000; - -/// Opcode flag meaning has size field, skip and continue. -#[allow(unused)] -const OPCODE_FLAG_SKIP: u32 = 0x40000000; - -/// Denotes type of code requirements. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -#[repr(u32)] -pub enum RequirementType { - /// What hosts may run on us. - Host, - /// What guests we may run. - Guest, - /// Designated requirement. - Designated, - /// What libraries we may link against. - Library, - /// What plug-ins we may load. - Plugin, - /// Unknown requirement type. - Unknown(u32), -} - -impl From for RequirementType { - fn from(v: u32) -> Self { - match v { - 1 => Self::Host, - 2 => Self::Guest, - 3 => Self::Designated, - 4 => Self::Library, - 5 => Self::Plugin, - _ => Self::Unknown(v), - } - } -} - -impl From for u32 { - fn from(t: RequirementType) -> Self { - match t { - RequirementType::Host => 1, - RequirementType::Guest => 2, - RequirementType::Designated => 3, - RequirementType::Library => 4, - RequirementType::Plugin => 5, - RequirementType::Unknown(v) => v, - } - } -} - -impl PartialOrd for RequirementType { - fn partial_cmp(&self, other: &Self) -> Option { - u32::from(*self).partial_cmp(&u32::from(*other)) - } -} - -impl Ord for RequirementType { - fn cmp(&self, other: &Self) -> Ordering { - u32::from(*self).cmp(&u32::from(*other)) - } -} - -impl std::fmt::Display for RequirementType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Host => f.write_str("host(1)"), - Self::Guest => f.write_str("guest(2)"), - Self::Designated => f.write_str("designated(3)"), - Self::Library => f.write_str("library(4)"), - Self::Plugin => f.write_str("plugin(5)"), - Self::Unknown(v) => f.write_fmt(format_args!("unknown({})", v)), - } - } -} - -fn read_data(data: &[u8]) -> Result<(&[u8], &[u8]), AppleCodesignError> { - let length = data.pread_with::(0, scroll::BE)?; - let value = &data[4..4 + length as usize]; - - // Next element is aligned on next 4 byte boundary. - let offset = 4 + length as usize; - - let offset = match offset % 4 { - 0 => offset, - extra => offset + 4 - extra, - }; - - let remaining = &data[offset..]; - - Ok((value, remaining)) -} - -fn write_data(dest: &mut impl Write, data: &[u8]) -> Result<(), AppleCodesignError> { - dest.iowrite_with(data.len() as u32, scroll::BE)?; - dest.write_all(data)?; - - match data.len() % 4 { - 0 => {} - pad => { - for _ in 0..4 - pad { - dest.iowrite(0u8)?; - } - } - } - - Ok(()) -} - -/// Format a certificate slot's value to human form. -fn format_certificate_slot(slot: i32) -> String { - match slot { - -1 => "root".to_string(), - 0 => "leaf".to_string(), - _ => format!("{}", slot), - } -} - -/// A value in a code requirement expression. -/// -/// The value can be various primitive types. This type exists to make it -/// easier to work with and format values in code requirement expressions. -#[derive(Clone, Debug, PartialEq)] -pub enum CodeRequirementValue<'a> { - String(Cow<'a, str>), - Bytes(Cow<'a, [u8]>), -} - -impl<'a> From<&'a [u8]> for CodeRequirementValue<'a> { - fn from(value: &'a [u8]) -> Self { - let is_ascii_printable = |c: &u8| -> bool { - c.is_ascii_alphanumeric() || c.is_ascii_whitespace() || c.is_ascii_punctuation() - }; - - if value.iter().all(is_ascii_printable) { - Self::String(unsafe { std::str::from_utf8_unchecked(value) }.into()) - } else { - Self::Bytes(value.into()) - } - } -} - -impl<'a> From<&'a str> for CodeRequirementValue<'a> { - fn from(s: &'a str) -> Self { - Self::String(s.into()) - } -} - -impl<'a> From> for CodeRequirementValue<'a> { - fn from(v: Cow<'a, str>) -> Self { - Self::String(v) - } -} - -impl<'a> From for CodeRequirementValue<'static> { - fn from(v: String) -> Self { - Self::String(Cow::Owned(v)) - } -} - -impl<'a> Display for CodeRequirementValue<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::String(s) => f.write_str(s), - Self::Bytes(data) => f.write_fmt(format_args!("{}", hex::encode(data))), - } - } -} - -impl<'a> CodeRequirementValue<'a> { - /// Write the encoded version of this value somewhere. - /// - /// Binary encoding is u32 of length, then raw bytes, then NULL padding to next u32. - fn write_encoded(&self, dest: &mut impl Write) -> Result<(), AppleCodesignError> { - match self { - Self::Bytes(data) => write_data(dest, data), - Self::String(s) => write_data(dest, s.as_bytes()), - } - } -} - -/// An opcode representing a code requirement expression. -#[derive(Clone, Copy, Debug, PartialEq)] -#[repr(u32)] -enum RequirementOpCode { - False = 0, - True = 1, - Identifier = 2, - AnchorApple = 3, - AnchorCertificateHash = 4, - InfoKeyValueLegacy = 5, - And = 6, - Or = 7, - CodeDirectoryHash = 8, - Not = 9, - InfoPlistExpression = 10, - CertificateField = 11, - CertificateTrusted = 12, - AnchorTrusted = 13, - CertificateGeneric = 14, - AnchorAppleGeneric = 15, - EntitlementsField = 16, - CertificatePolicy = 17, - NamedAnchor = 18, - NamedCode = 19, - Platform = 20, - Notarized = 21, - CertificateFieldDate = 22, - LegacyDeveloperId = 23, -} - -impl TryFrom for RequirementOpCode { - type Error = AppleCodesignError; - - fn try_from(v: u32) -> Result { - match v { - 0 => Ok(Self::False), - 1 => Ok(Self::True), - 2 => Ok(Self::Identifier), - 3 => Ok(Self::AnchorApple), - 4 => Ok(Self::AnchorCertificateHash), - 5 => Ok(Self::InfoKeyValueLegacy), - 6 => Ok(Self::And), - 7 => Ok(Self::Or), - 8 => Ok(Self::CodeDirectoryHash), - 9 => Ok(Self::Not), - 10 => Ok(Self::InfoPlistExpression), - 11 => Ok(Self::CertificateField), - 12 => Ok(Self::CertificateTrusted), - 13 => Ok(Self::AnchorTrusted), - 14 => Ok(Self::CertificateGeneric), - 15 => Ok(Self::AnchorAppleGeneric), - 16 => Ok(Self::EntitlementsField), - 17 => Ok(Self::CertificatePolicy), - 18 => Ok(Self::NamedAnchor), - 19 => Ok(Self::NamedCode), - 20 => Ok(Self::Platform), - 21 => Ok(Self::Notarized), - 22 => Ok(Self::CertificateFieldDate), - 23 => Ok(Self::LegacyDeveloperId), - _ => Err(AppleCodesignError::RequirementUnknownOpcode(v)), - } - } -} - -impl RequirementOpCode { - /// Parse the payload of an opcode. - /// - /// On successful parse, returns an [CodeRequirementExpression] and remaining data in - /// the input slice. - pub fn parse_payload<'a>( - &self, - data: &'a [u8], - ) -> Result<(CodeRequirementExpression<'a>, &'a [u8]), AppleCodesignError> { - match self { - Self::False => Ok((CodeRequirementExpression::False, data)), - Self::True => Ok((CodeRequirementExpression::True, data)), - Self::Identifier => { - let (value, data) = read_data(data)?; - let s = std::str::from_utf8(value).map_err(|_| { - AppleCodesignError::RequirementMalformed("identifier value not a UTF-8 string") - })?; - - Ok((CodeRequirementExpression::Identifier(Cow::from(s)), data)) - } - Self::AnchorApple => Ok((CodeRequirementExpression::AnchorApple, data)), - Self::AnchorCertificateHash => { - let slot = data.pread_with::(0, scroll::BE)?; - let digest_length = data.pread_with::(4, scroll::BE)?; - let digest = &data[8..8 + digest_length as usize]; - - Ok(( - CodeRequirementExpression::AnchorCertificateHash(slot, digest.into()), - &data[8 + digest_length as usize..], - )) - } - Self::InfoKeyValueLegacy => { - let (key, data) = read_data(data)?; - - let key = std::str::from_utf8(key).map_err(|_| { - AppleCodesignError::RequirementMalformed("info key not a UTF-8 string") - })?; - - let (value, data) = read_data(data)?; - - let value = std::str::from_utf8(value).map_err(|_| { - AppleCodesignError::RequirementMalformed("info value not a UTF-8 string") - })?; - - Ok(( - CodeRequirementExpression::InfoKeyValueLegacy(key.into(), value.into()), - data, - )) - } - Self::And => { - let (a, data) = CodeRequirementExpression::from_bytes(data)?; - let (b, data) = CodeRequirementExpression::from_bytes(data)?; - - Ok(( - CodeRequirementExpression::And(Box::new(a), Box::new(b)), - data, - )) - } - Self::Or => { - let (a, data) = CodeRequirementExpression::from_bytes(data)?; - let (b, data) = CodeRequirementExpression::from_bytes(data)?; - - Ok(( - CodeRequirementExpression::Or(Box::new(a), Box::new(b)), - data, - )) - } - Self::CodeDirectoryHash => { - let (value, data) = read_data(data)?; - - Ok(( - CodeRequirementExpression::CodeDirectoryHash(value.into()), - data, - )) - } - Self::Not => { - let (expr, data) = CodeRequirementExpression::from_bytes(data)?; - - Ok((CodeRequirementExpression::Not(Box::new(expr)), data)) - } - Self::InfoPlistExpression => { - let (key, data) = read_data(data)?; - - let key = std::str::from_utf8(key).map_err(|_| { - AppleCodesignError::RequirementMalformed("key is not valid UTF-8") - })?; - - let (expr, data) = CodeRequirementMatchExpression::from_bytes(data)?; - - Ok(( - CodeRequirementExpression::InfoPlistKeyField(key.into(), expr), - data, - )) - } - Self::CertificateField => { - let slot = data.pread_with::(0, scroll::BE)?; - - let (field, data) = read_data(&data[4..])?; - - let field = std::str::from_utf8(field).map_err(|_| { - AppleCodesignError::RequirementMalformed("certificate field is not valid UTF-8") - })?; - - let (expr, data) = CodeRequirementMatchExpression::from_bytes(data)?; - - Ok(( - CodeRequirementExpression::CertificateField(slot, field.into(), expr), - data, - )) - } - Self::CertificateTrusted => { - let slot = data.pread_with::(0, scroll::BE)?; - - Ok(( - CodeRequirementExpression::CertificateTrusted(slot), - &data[4..], - )) - } - Self::AnchorTrusted => Ok((CodeRequirementExpression::AnchorTrusted, data)), - Self::CertificateGeneric => { - let slot = data.pread_with::(0, scroll::BE)?; - - let (oid, data) = read_data(&data[4..])?; - - let (expr, data) = CodeRequirementMatchExpression::from_bytes(data)?; - - Ok(( - CodeRequirementExpression::CertificateGeneric(slot, Oid(oid), expr), - data, - )) - } - Self::AnchorAppleGeneric => Ok((CodeRequirementExpression::AnchorAppleGeneric, data)), - Self::EntitlementsField => { - let (key, data) = read_data(data)?; - - let key = std::str::from_utf8(key).map_err(|_| { - AppleCodesignError::RequirementMalformed("entitlement key is not UTF-8") - })?; - - let (expr, data) = CodeRequirementMatchExpression::from_bytes(data)?; - - Ok(( - CodeRequirementExpression::EntitlementsKey(key.into(), expr), - data, - )) - } - Self::CertificatePolicy => { - let slot = data.pread_with::(0, scroll::BE)?; - - let (oid, data) = read_data(&data[4..])?; - - let (expr, data) = CodeRequirementMatchExpression::from_bytes(data)?; - - Ok(( - CodeRequirementExpression::CertificatePolicy(slot, Oid(oid), expr), - data, - )) - } - Self::NamedAnchor => { - let (name, data) = read_data(data)?; - - let name = std::str::from_utf8(name).map_err(|_| { - AppleCodesignError::RequirementMalformed("named anchor isn't UTF-8") - })?; - - Ok((CodeRequirementExpression::NamedAnchor(name.into()), data)) - } - Self::NamedCode => { - let (name, data) = read_data(data)?; - - let name = std::str::from_utf8(name).map_err(|_| { - AppleCodesignError::RequirementMalformed("named code isn't UTF-8") - })?; - - Ok((CodeRequirementExpression::NamedCode(name.into()), data)) - } - Self::Platform => { - let value = data.pread_with::(0, scroll::BE)?; - - Ok((CodeRequirementExpression::Platform(value), &data[4..])) - } - Self::Notarized => Ok((CodeRequirementExpression::Notarized, data)), - Self::CertificateFieldDate => { - let slot = data.pread_with::(0, scroll::BE)?; - - let (oid, data) = read_data(&data[4..])?; - - let (expr, data) = CodeRequirementMatchExpression::from_bytes(data)?; - - Ok(( - CodeRequirementExpression::CertificateFieldDate(slot, Oid(oid), expr), - data, - )) - } - Self::LegacyDeveloperId => Ok((CodeRequirementExpression::LegacyDeveloperId, data)), - } - } -} - -/// Defines a code requirement expression. -#[derive(Clone, Debug, PartialEq)] -pub enum CodeRequirementExpression<'a> { - /// False - /// - /// `false` - /// - /// No payload. - False, - - /// True - /// - /// `true` - /// - /// No payload. - True, - - /// Signing identifier. - /// - /// `identifier ` - /// - /// 4 bytes length followed by C string. - Identifier(Cow<'a, str>), - - /// The certificate chain must lead to an Apple root. - /// - /// `anchor apple` - /// - /// No payload. - AnchorApple, - - /// The certificate chain must anchor to a certificate with specified SHA-1 hash. - /// - /// `anchor H""` - /// - /// 4 bytes slot number, 4 bytes hash length, hash value. - AnchorCertificateHash(i32, Cow<'a, [u8]>), - - /// Info.plist key value (legacy). - /// - /// `info[] = ` - /// - /// 2 pairs of (length + value). - InfoKeyValueLegacy(Cow<'a, str>, Cow<'a, str>), - - /// Logical and. - /// - /// `expr0 and expr1` - /// - /// Payload consists of 2 sub-expressions with no additional encoding. - And( - Box>, - Box>, - ), - - /// Logical or. - /// - /// `expr0 or expr1` - /// - /// Payload consists of 2 sub-expressions with no additional encoding. - Or( - Box>, - Box>, - ), - - /// Code directory hash. - /// - /// `cdhash H"" - /// - /// 4 bytes length followed by raw digest value. - CodeDirectoryHash(Cow<'a, [u8]>), - - /// Logical not. - /// - /// `!expr` - /// - /// Payload is 1 sub-expression. - Not(Box>), - - /// Info plist key field. - /// - /// `info [key] match expression` - /// - /// e.g. `info [CFBundleName] exists` - /// - /// 4 bytes key length, key string, then match expression. - InfoPlistKeyField(Cow<'a, str>, CodeRequirementMatchExpression<'a>), - - /// Certificate field matches. - /// - /// `certificate [] match expression` - /// - /// Slot i32, 4 bytes field length, field string, then match expression. - CertificateField(i32, Cow<'a, str>, CodeRequirementMatchExpression<'a>), - - /// Certificate in position is trusted for code signing. - /// - /// `certificate trusted` - /// - /// 4 bytes certificate position. - CertificateTrusted(i32), - - /// The certificate chain must lead to a trusted root. - /// - /// `anchor trusted` - /// - /// No payload. - AnchorTrusted, - - /// Certificate field matches by OID. - /// - /// `certificate [field.] match expression` - /// - /// Slot i32, 4 bytes OID length, OID raw bytes, match expression. - CertificateGeneric(i32, Oid<&'a [u8]>, CodeRequirementMatchExpression<'a>), - - /// For code signed by Apple, including from code signing certificates issued by Apple. - /// - /// `anchor apple generic` - /// - /// No payload. - AnchorAppleGeneric, - - /// Value associated with specified key in signature's embedded entitlements dictionary. - /// - /// `entitlement [] match expression` - /// - /// 4 bytes key length, key bytes, match expression. - EntitlementsKey(Cow<'a, str>, CodeRequirementMatchExpression<'a>), - - /// OID associated with certificate in a given slot. - /// - /// It is unknown what the OID means. - /// - /// `certificate [policy.] match expression` - CertificatePolicy(i32, Oid<&'a [u8]>, CodeRequirementMatchExpression<'a>), - - /// A named Apple anchor. - /// - /// `anchor apple ` - /// - /// 4 bytes name length, name bytes. - NamedAnchor(Cow<'a, str>), - - /// Named code. - /// - /// `()` - /// - /// 4 bytes name length, name bytes. - NamedCode(Cow<'a, str>), - - /// Platform value. - /// - /// `platform = ` - /// - /// Payload is a u32. - Platform(u32), - - /// Binary is notarized. - /// - /// `notarized` - /// - /// No Payload. - Notarized, - - /// Certificate field date. - /// - /// Unknown what the OID corresponds to. - /// - /// `certificate [timestamp.] match expression` - CertificateFieldDate(i32, Oid<&'a [u8]>, CodeRequirementMatchExpression<'a>), - - /// Legacy developer ID used. - LegacyDeveloperId, -} - -impl<'a> Display for CodeRequirementExpression<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::False => f.write_str("never"), - Self::True => f.write_str("always"), - Self::Identifier(value) => f.write_fmt(format_args!("identifier \"{}\"", value)), - Self::AnchorApple => f.write_str("anchor apple"), - Self::AnchorCertificateHash(slot, digest) => { - f.write_fmt(format_args!("anchor {} H\"{}\"", slot, hex::encode(digest))) - } - Self::InfoKeyValueLegacy(key, value) => { - f.write_fmt(format_args!("info[{}] = \"{}\"", key, value)) - } - Self::And(a, b) => f.write_fmt(format_args!("({}) and ({})", a, b)), - Self::Or(a, b) => f.write_fmt(format_args!("({}) or ({})", a, b)), - Self::CodeDirectoryHash(digest) => { - f.write_fmt(format_args!("cdhash H\"{}\"", hex::encode(digest))) - } - Self::Not(expr) => f.write_fmt(format_args!("!({})", expr)), - Self::InfoPlistKeyField(key, expr) => { - f.write_fmt(format_args!("info [{}] {}", key, expr)) - } - Self::CertificateField(slot, field, expr) => f.write_fmt(format_args!( - "certificate {}[{}] {}", - format_certificate_slot(*slot), - field, - expr - )), - Self::CertificateTrusted(slot) => { - f.write_fmt(format_args!("certificate {} trusted", slot)) - } - Self::AnchorTrusted => f.write_str("anchor trusted"), - Self::CertificateGeneric(slot, oid, expr) => f.write_fmt(format_args!( - "certificate {}[field.{}] {}", - format_certificate_slot(*slot), - oid, - expr - )), - Self::AnchorAppleGeneric => f.write_str("anchor apple generic"), - Self::EntitlementsKey(key, expr) => { - f.write_fmt(format_args!("entitlement [{}] {}", key, expr)) - } - Self::CertificatePolicy(slot, oid, expr) => f.write_fmt(format_args!( - "certificate {}[policy.{}] {}", - format_certificate_slot(*slot), - oid, - expr - )), - Self::NamedAnchor(name) => f.write_fmt(format_args!("anchor apple {}", name)), - Self::NamedCode(name) => f.write_fmt(format_args!("({})", name)), - Self::Platform(platform) => f.write_fmt(format_args!("platform = {}", platform)), - Self::Notarized => f.write_str("notarized"), - Self::CertificateFieldDate(slot, oid, expr) => f.write_fmt(format_args!( - "certificate {}[timestamp.{}] {}", - format_certificate_slot(*slot), - oid, - expr - )), - Self::LegacyDeveloperId => f.write_str("legacy"), - } - } -} - -impl<'a> From<&CodeRequirementExpression<'a>> for RequirementOpCode { - fn from(e: &CodeRequirementExpression) -> Self { - match e { - CodeRequirementExpression::False => RequirementOpCode::False, - CodeRequirementExpression::True => RequirementOpCode::True, - CodeRequirementExpression::Identifier(_) => RequirementOpCode::Identifier, - CodeRequirementExpression::AnchorApple => RequirementOpCode::AnchorApple, - CodeRequirementExpression::AnchorCertificateHash(_, _) => { - RequirementOpCode::AnchorCertificateHash - } - CodeRequirementExpression::InfoKeyValueLegacy(_, _) => { - RequirementOpCode::InfoKeyValueLegacy - } - CodeRequirementExpression::And(_, _) => RequirementOpCode::And, - CodeRequirementExpression::Or(_, _) => RequirementOpCode::Or, - CodeRequirementExpression::CodeDirectoryHash(_) => RequirementOpCode::CodeDirectoryHash, - CodeRequirementExpression::Not(_) => RequirementOpCode::Not, - CodeRequirementExpression::InfoPlistKeyField(_, _) => { - RequirementOpCode::InfoPlistExpression - } - CodeRequirementExpression::CertificateField(_, _, _) => { - RequirementOpCode::CertificateField - } - CodeRequirementExpression::CertificateTrusted(_) => { - RequirementOpCode::CertificateTrusted - } - CodeRequirementExpression::AnchorTrusted => RequirementOpCode::AnchorTrusted, - CodeRequirementExpression::CertificateGeneric(_, _, _) => { - RequirementOpCode::CertificateGeneric - } - CodeRequirementExpression::AnchorAppleGeneric => RequirementOpCode::AnchorAppleGeneric, - CodeRequirementExpression::EntitlementsKey(_, _) => { - RequirementOpCode::EntitlementsField - } - CodeRequirementExpression::CertificatePolicy(_, _, _) => { - RequirementOpCode::CertificatePolicy - } - CodeRequirementExpression::NamedAnchor(_) => RequirementOpCode::NamedAnchor, - CodeRequirementExpression::NamedCode(_) => RequirementOpCode::NamedCode, - CodeRequirementExpression::Platform(_) => RequirementOpCode::Platform, - CodeRequirementExpression::Notarized => RequirementOpCode::Notarized, - CodeRequirementExpression::CertificateFieldDate(_, _, _) => { - RequirementOpCode::CertificateFieldDate - } - CodeRequirementExpression::LegacyDeveloperId => RequirementOpCode::LegacyDeveloperId, - } - } -} - -impl<'a> CodeRequirementExpression<'a> { - /// Construct an expression element by reading from a slice. - /// - /// Returns the newly constructed element and remaining data in the slice. - pub fn from_bytes(data: &'a [u8]) -> Result<(Self, &'a [u8]), AppleCodesignError> { - let opcode_raw = data.pread_with::(0, scroll::BE)?; - - let _flags = opcode_raw & OPCODE_FLAG_MASK; - let opcode = opcode_raw & OPCODE_VALUE_MASK; - - let data = &data[4..]; - - let opcode = RequirementOpCode::try_from(opcode)?; - - opcode.parse_payload(data) - } - - /// Write binary representation of this expression to a destination. - pub fn write_to(&self, dest: &mut impl Write) -> Result<(), AppleCodesignError> { - dest.iowrite_with(RequirementOpCode::from(self) as u32, scroll::BE)?; - - match self { - Self::False => {} - Self::True => {} - Self::Identifier(s) => { - write_data(dest, s.as_bytes())?; - } - Self::AnchorApple => {} - Self::AnchorCertificateHash(slot, hash) => { - dest.iowrite_with(*slot, scroll::BE)?; - write_data(dest, hash)?; - } - Self::InfoKeyValueLegacy(key, value) => { - write_data(dest, key.as_bytes())?; - write_data(dest, value.as_bytes())?; - } - Self::And(a, b) => { - a.write_to(dest)?; - b.write_to(dest)?; - } - Self::Or(a, b) => { - a.write_to(dest)?; - b.write_to(dest)?; - } - Self::CodeDirectoryHash(hash) => { - write_data(dest, hash)?; - } - Self::Not(expr) => { - expr.write_to(dest)?; - } - Self::InfoPlistKeyField(key, m) => { - write_data(dest, key.as_bytes())?; - m.write_to(dest)?; - } - Self::CertificateField(slot, field, m) => { - dest.iowrite_with(*slot, scroll::BE)?; - write_data(dest, field.as_bytes())?; - m.write_to(dest)?; - } - Self::CertificateTrusted(slot) => { - dest.iowrite_with(*slot, scroll::BE)?; - } - Self::AnchorTrusted => {} - Self::CertificateGeneric(slot, oid, m) => { - dest.iowrite_with(*slot, scroll::BE)?; - write_data(dest, oid.as_ref())?; - m.write_to(dest)?; - } - Self::AnchorAppleGeneric => {} - Self::EntitlementsKey(key, m) => { - write_data(dest, key.as_bytes())?; - m.write_to(dest)?; - } - Self::CertificatePolicy(slot, oid, m) => { - dest.iowrite_with(*slot, scroll::BE)?; - write_data(dest, oid.as_ref())?; - m.write_to(dest)?; - } - Self::NamedAnchor(value) => { - write_data(dest, value.as_bytes())?; - } - Self::NamedCode(value) => { - write_data(dest, value.as_bytes())?; - } - Self::Platform(value) => { - dest.iowrite_with(*value, scroll::BE)?; - } - Self::Notarized => {} - Self::CertificateFieldDate(slot, oid, m) => { - dest.iowrite_with(*slot, scroll::BE)?; - write_data(dest, oid.as_ref())?; - m.write_to(dest)?; - } - Self::LegacyDeveloperId => {} - } - - Ok(()) - } - - /// Produce the binary serialization of this expression. - /// - /// The blob header/magic is not included. - pub fn to_bytes(&self) -> Result, AppleCodesignError> { - let mut res = vec![]; - - self.write_to(&mut res)?; - - Ok(res) - } -} - -/// A code requirement match expression type. -#[derive(Clone, Copy, Debug, PartialEq)] -#[repr(u32)] -enum MatchType { - Exists = 0, - Equal = 1, - Contains = 2, - BeginsWith = 3, - EndsWith = 4, - LessThan = 5, - GreaterThan = 6, - LessThanEqual = 7, - GreaterThanEqual = 8, - On = 9, - Before = 10, - After = 11, - OnOrBefore = 12, - OnOrAfter = 13, - Absent = 14, -} - -impl TryFrom for MatchType { - type Error = AppleCodesignError; - - fn try_from(v: u32) -> Result { - match v { - 0 => Ok(Self::Exists), - 1 => Ok(Self::Equal), - 2 => Ok(Self::Contains), - 3 => Ok(Self::BeginsWith), - 4 => Ok(Self::EndsWith), - 5 => Ok(Self::LessThan), - 6 => Ok(Self::GreaterThan), - 7 => Ok(Self::LessThanEqual), - 8 => Ok(Self::GreaterThanEqual), - 9 => Ok(Self::On), - 10 => Ok(Self::Before), - 11 => Ok(Self::After), - 12 => Ok(Self::OnOrBefore), - 13 => Ok(Self::OnOrAfter), - 14 => Ok(Self::Absent), - _ => Err(AppleCodesignError::RequirementUnknownMatchExpression(v)), - } - } -} - -impl MatchType { - /// Parse the payload of a match expression. - pub fn parse_payload<'a>( - &self, - data: &'a [u8], - ) -> Result<(CodeRequirementMatchExpression<'a>, &'a [u8]), AppleCodesignError> { - match self { - Self::Exists => Ok((CodeRequirementMatchExpression::Exists, data)), - Self::Equal => { - let (value, data) = read_data(data)?; - - Ok((CodeRequirementMatchExpression::Equal(value.into()), data)) - } - Self::Contains => { - let (value, data) = read_data(data)?; - - Ok((CodeRequirementMatchExpression::Contains(value.into()), data)) - } - Self::BeginsWith => { - let (value, data) = read_data(data)?; - - Ok(( - CodeRequirementMatchExpression::BeginsWith(value.into()), - data, - )) - } - Self::EndsWith => { - let (value, data) = read_data(data)?; - - Ok((CodeRequirementMatchExpression::EndsWith(value.into()), data)) - } - Self::LessThan => { - let (value, data) = read_data(data)?; - - Ok((CodeRequirementMatchExpression::LessThan(value.into()), data)) - } - Self::GreaterThan => { - let (value, data) = read_data(data)?; - - Ok(( - CodeRequirementMatchExpression::GreaterThan(value.into()), - data, - )) - } - Self::LessThanEqual => { - let (value, data) = read_data(data)?; - - Ok(( - CodeRequirementMatchExpression::LessThanEqual(value.into()), - data, - )) - } - Self::GreaterThanEqual => { - let (value, data) = read_data(data)?; - - Ok(( - CodeRequirementMatchExpression::GreaterThanEqual(value.into()), - data, - )) - } - Self::On => { - let value = data.pread_with::(0, scroll::BE)?; - - Ok(( - CodeRequirementMatchExpression::On(chrono::Utc.timestamp(value, 0)), - &data[8..], - )) - } - Self::Before => { - let value = data.pread_with::(0, scroll::BE)?; - - Ok(( - CodeRequirementMatchExpression::Before(chrono::Utc.timestamp(value, 0)), - &data[8..], - )) - } - Self::After => { - let value = data.pread_with::(0, scroll::BE)?; - - Ok(( - CodeRequirementMatchExpression::After(chrono::Utc.timestamp(value, 0)), - &data[8..], - )) - } - Self::OnOrBefore => { - let value = data.pread_with::(0, scroll::BE)?; - - Ok(( - CodeRequirementMatchExpression::OnOrBefore(chrono::Utc.timestamp(value, 0)), - &data[8..], - )) - } - Self::OnOrAfter => { - let value = data.pread_with::(0, scroll::BE)?; - - Ok(( - CodeRequirementMatchExpression::OnOrAfter(chrono::Utc.timestamp(value, 0)), - &data[8..], - )) - } - Self::Absent => Ok((CodeRequirementMatchExpression::Absent, data)), - } - } -} - -/// An instance of a match expression in a [CodeRequirementExpression]. -#[derive(Clone, Debug, PartialEq)] -pub enum CodeRequirementMatchExpression<'a> { - /// Entity exists. - /// - /// `exists` - /// - /// No payload. - Exists, - - /// Equality. - /// - /// `= ` - /// - /// 4 bytes length, raw data. - Equal(CodeRequirementValue<'a>), - - /// Contains. - /// - /// `~ ` - /// - /// 4 bytes length, raw data. - Contains(CodeRequirementValue<'a>), - - /// Begins with. - /// - /// `= *` - /// - /// 4 bytes length, raw data. - BeginsWith(CodeRequirementValue<'a>), - - /// Ends with. - /// - /// `= *` - /// - /// 4 bytes length, raw data. - EndsWith(CodeRequirementValue<'a>), - - /// Less than. - /// - /// `< ` - /// - /// 4 bytes length, raw data. - LessThan(CodeRequirementValue<'a>), - - /// Greater than. - /// - /// `> ` - GreaterThan(CodeRequirementValue<'a>), - - /// Less than or equal to. - /// - /// `<= ` - /// - /// 4 bytes length, raw data. - LessThanEqual(CodeRequirementValue<'a>), - - /// Greater than or equal to. - /// - /// `>= ` - /// - /// 4 bytes length, raw data. - GreaterThanEqual(CodeRequirementValue<'a>), - - /// Timestamp value equivalent. - /// - /// `= timestamp ""` - On(chrono::DateTime), - - /// Timestamp value before. - /// - /// `< timestamp ""` - Before(chrono::DateTime), - - /// Timestamp value after. - /// - /// `> timestamp ""` - After(chrono::DateTime), - - /// Timestamp value equivalent or before. - /// - /// `<= timestamp ""` - OnOrBefore(chrono::DateTime), - - /// Timestamp value equivalent or after. - /// - /// `>= timestamp ""` - OnOrAfter(chrono::DateTime), - - /// Value is absent. - /// - /// `` - /// - /// No payload. - Absent, -} - -impl<'a> Display for CodeRequirementMatchExpression<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Exists => f.write_str("/* exists */"), - Self::Equal(value) => f.write_fmt(format_args!("= \"{}\"", value)), - Self::Contains(value) => f.write_fmt(format_args!("~ \"{}\"", value)), - Self::BeginsWith(value) => f.write_fmt(format_args!("= \"{}*\"", value)), - Self::EndsWith(value) => f.write_fmt(format_args!("= \"*{}\"", value)), - Self::LessThan(value) => f.write_fmt(format_args!("< \"{}\"", value)), - Self::GreaterThan(value) => f.write_fmt(format_args!("> \"{}\"", value)), - Self::LessThanEqual(value) => f.write_fmt(format_args!("<= \"{}\"", value)), - Self::GreaterThanEqual(value) => f.write_fmt(format_args!(">= \"{}\"", value)), - Self::On(value) => f.write_fmt(format_args!("= \"{}\"", value)), - Self::Before(value) => f.write_fmt(format_args!("< \"{}\"", value)), - Self::After(value) => f.write_fmt(format_args!("> \"{}\"", value)), - Self::OnOrBefore(value) => f.write_fmt(format_args!("<= \"{}\"", value)), - Self::OnOrAfter(value) => f.write_fmt(format_args!(">= \"{}\"", value)), - Self::Absent => f.write_str("absent"), - } - } -} - -impl<'a> From<&CodeRequirementMatchExpression<'a>> for MatchType { - fn from(m: &CodeRequirementMatchExpression<'a>) -> Self { - match m { - CodeRequirementMatchExpression::Exists => MatchType::Exists, - CodeRequirementMatchExpression::Equal(_) => MatchType::Equal, - CodeRequirementMatchExpression::Contains(_) => MatchType::Contains, - CodeRequirementMatchExpression::BeginsWith(_) => MatchType::BeginsWith, - CodeRequirementMatchExpression::EndsWith(_) => MatchType::EndsWith, - CodeRequirementMatchExpression::LessThan(_) => MatchType::LessThan, - CodeRequirementMatchExpression::GreaterThan(_) => MatchType::GreaterThan, - CodeRequirementMatchExpression::LessThanEqual(_) => MatchType::LessThanEqual, - CodeRequirementMatchExpression::GreaterThanEqual(_) => MatchType::GreaterThanEqual, - CodeRequirementMatchExpression::On(_) => MatchType::On, - CodeRequirementMatchExpression::Before(_) => MatchType::Before, - CodeRequirementMatchExpression::After(_) => MatchType::After, - CodeRequirementMatchExpression::OnOrBefore(_) => MatchType::OnOrBefore, - CodeRequirementMatchExpression::OnOrAfter(_) => MatchType::OnOrAfter, - CodeRequirementMatchExpression::Absent => MatchType::Absent, - } - } -} - -impl<'a> CodeRequirementMatchExpression<'a> { - /// Parse a match expression from bytes. - /// - /// The slice should begin with the match type u32. - pub fn from_bytes(data: &'a [u8]) -> Result<(Self, &'a [u8]), AppleCodesignError> { - let typ = data.pread_with::(0, scroll::BE)?; - - let typ = MatchType::try_from(typ)?; - - typ.parse_payload(&data[4..]) - } - - /// Write binary representation of this match expression to a destination. - pub fn write_to(&self, dest: &mut impl Write) -> Result<(), AppleCodesignError> { - dest.iowrite_with(MatchType::from(self) as u32, scroll::BE)?; - - match self { - Self::Exists => {} - Self::Equal(value) => value.write_encoded(dest)?, - Self::Contains(value) => value.write_encoded(dest)?, - Self::BeginsWith(value) => value.write_encoded(dest)?, - Self::EndsWith(value) => value.write_encoded(dest)?, - Self::LessThan(value) => value.write_encoded(dest)?, - Self::GreaterThan(value) => value.write_encoded(dest)?, - Self::LessThanEqual(value) => value.write_encoded(dest)?, - Self::GreaterThanEqual(value) => value.write_encoded(dest)?, - Self::On(value) => dest.iowrite_with(value.timestamp(), scroll::BE)?, - Self::Before(value) => dest.iowrite_with(value.timestamp(), scroll::BE)?, - Self::After(value) => dest.iowrite_with(value.timestamp(), scroll::BE)?, - Self::OnOrBefore(value) => dest.iowrite_with(value.timestamp(), scroll::BE)?, - Self::OnOrAfter(value) => dest.iowrite_with(value.timestamp(), scroll::BE)?, - Self::Absent => {} - } - - Ok(()) - } -} - -/// Represents a series of [CodeRequirementExpression]. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct CodeRequirements<'a>(Vec>); - -impl<'a> Deref for CodeRequirements<'a> { - type Target = Vec>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a> DerefMut for CodeRequirements<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl<'a> Display for CodeRequirements<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for (i, expr) in self.0.iter().enumerate() { - f.write_fmt(format_args!("{}: {};", i, expr))?; - } - - Ok(()) - } -} - -impl<'a> From>> for CodeRequirements<'a> { - fn from(v: Vec>) -> Self { - Self(v) - } -} - -impl<'a> CodeRequirements<'a> { - /// Parse the binary serialization of code requirements. - /// - /// This parses the data that follows the requirement blob header/magic that - /// usually accompanies the binary representation of code requirements. - pub fn parse_binary(data: &'a [u8]) -> Result<(Self, &'a [u8]), AppleCodesignError> { - let count = data.pread_with::(0, scroll::BE)?; - let mut data = &data[4..]; - - let mut elements = Vec::with_capacity(count as usize); - - for _ in 0..count { - let res = CodeRequirementExpression::from_bytes(data)?; - - elements.push(res.0); - data = res.1; - } - - Ok((Self(elements), data)) - } - - /// Parse a code requirement blob, which begins with header magic. - /// - /// This can be used to parse the output generated by `csreq -b`. - pub fn parse_blob(data: &'a [u8]) -> Result<(Self, &'a [u8]), AppleCodesignError> { - let data = read_and_validate_blob_header( - data, - u32::from(CodeSigningMagic::Requirement), - "code requirement blob", - ) - .map_err(|_| AppleCodesignError::RequirementMalformed("blob header"))?; - - Self::parse_binary(data) - } - - /// Write binary representation of these expressions to a destination. - /// - /// The blob header/magic is not written. - pub fn write_to(&self, dest: &mut impl Write) -> Result<(), AppleCodesignError> { - dest.iowrite_with(self.0.len() as u32, scroll::BE)?; - for e in &self.0 { - e.write_to(dest)?; - } - - Ok(()) - } - - /// Obtain the blob representation of these expressions. - /// - /// This is like [CodeRequirements.write_to] except it will return an owned Vec - /// and will prepend the blob header identifying the data as code requirements. - /// - /// The generated data should be equivalent to what `csreq -b` would produce. - pub fn to_blob_data(&self) -> Result, AppleCodesignError> { - let mut payload = vec![]; - self.write_to(&mut payload)?; - - let mut dest = Vec::with_capacity(payload.len() + 8); - dest.iowrite_with(u32::from(CodeSigningMagic::Requirement), scroll::BE)?; - dest.iowrite_with(dest.capacity() as u32, scroll::BE)?; - dest.write_all(&payload)?; - - Ok(dest) - } - - /// Have this instance occupy a slot in a [RequirementSetBlob] instance. - pub fn add_to_requirement_set( - &self, - requirements_set: &mut RequirementSetBlob, - slot: RequirementType, - ) -> Result<(), AppleCodesignError> { - let blob = RequirementBlob::try_from(self)?; - - requirements_set.set_requirements(slot, blob); - - Ok(()) - } -} - -impl<'a> TryFrom<&CodeRequirements<'a>> for RequirementBlob<'static> { - type Error = AppleCodesignError; - - fn try_from(requirements: &CodeRequirements<'a>) -> Result { - let mut data = Vec::::new(); - requirements.write_to(&mut data)?; - - Ok(Self { - data: Cow::Owned(data), - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - - fn verify_roundtrip(reqs: &CodeRequirements, source: &[u8]) { - let mut dest = Vec::::new(); - reqs.write_to(&mut dest).unwrap(); - assert_eq!(dest.as_slice(), source); - } - - #[test] - fn parse_false() { - let source = hex::decode("0000000100000000").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::False]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_true() { - let source = hex::decode("0000000100000001").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!(els, CodeRequirements(vec![CodeRequirementExpression::True])); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_identifier() { - let source = hex::decode("000000010000000200000007666f6f2e62617200").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::Identifier( - "foo.bar".into() - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_anchor_apple() { - let source = hex::decode("0000000100000003").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::AnchorApple]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_anchor_certificate_hash() { - let source = - hex::decode("0000000100000004ffffffff00000014deadbeefdeadbeefdeadbeefdeadbeefdeadbeef") - .unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::AnchorCertificateHash( - -1, - hex::decode("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef") - .unwrap() - .into() - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_and() { - let source = hex::decode("00000001000000060000000100000000").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::And( - Box::new(CodeRequirementExpression::True), - Box::new(CodeRequirementExpression::False) - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_or() { - let source = hex::decode("00000001000000070000000100000000").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::Or( - Box::new(CodeRequirementExpression::True), - Box::new(CodeRequirementExpression::False) - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_code_directory_hash() { - let source = - hex::decode("000000010000000800000014deadbeefdeadbeefdeadbeefdeadbeefdeadbeef") - .unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::CodeDirectoryHash( - hex::decode("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef") - .unwrap() - .into() - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_not() { - let source = hex::decode("000000010000000900000001").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::Not(Box::new( - CodeRequirementExpression::True - ))]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_info_plist_key_field() { - let source = hex::decode("000000010000000a000000036b65790000000000").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::Exists - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_certificate_field() { - let source = - hex::decode("000000010000000bffffffff0000000a7375626a6563742e434e000000000000") - .unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::CertificateField( - -1, - "subject.CN".into(), - CodeRequirementMatchExpression::Exists - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_certificate_trusted() { - let source = hex::decode("000000010000000cffffffff").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::CertificateTrusted(-1)]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_anchor_trusted() { - let source = hex::decode("000000010000000d").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::AnchorTrusted]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_certificate_generic() { - let source = hex::decode("000000010000000effffffff000000035504030000000000").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::CertificateGeneric( - -1, - Oid(&[0x55, 4, 3]), - CodeRequirementMatchExpression::Exists - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_anchor_apple_generic() { - let source = hex::decode("000000010000000f").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::AnchorAppleGeneric]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_entitlements_key() { - let source = hex::decode("0000000100000010000000036b65790000000000").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::EntitlementsKey( - "key".into(), - CodeRequirementMatchExpression::Exists - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_certificate_policy() { - let source = hex::decode("0000000100000011ffffffff000000035504030000000000").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::CertificatePolicy( - -1, - Oid(&[0x55, 4, 3]), - CodeRequirementMatchExpression::Exists - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_named_anchor() { - let source = hex::decode("000000010000001200000003666f6f00").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::NamedAnchor("foo".into())]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_named_code() { - let source = hex::decode("000000010000001300000003666f6f00").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::NamedCode("foo".into())]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_platform() { - let source = hex::decode("00000001000000140000000a").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::Platform(10)]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_notarized() { - let source = hex::decode("0000000100000015").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::Notarized]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_certificate_field_date() { - let source = hex::decode("0000000100000016ffffffff000000035504030000000000").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::CertificateFieldDate( - -1, - Oid(&[0x55, 4, 3]), - CodeRequirementMatchExpression::Exists, - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_legacy() { - let source = hex::decode("0000000100000017").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::LegacyDeveloperId]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_blob() { - let source = hex::decode("fade0c00000000100000000100000000").unwrap(); - - let (els, data) = CodeRequirements::parse_blob(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::False]) - ); - assert!(data.is_empty()); - - let dest = els.to_blob_data().unwrap(); - assert_eq!(source, dest); - } - - #[test] - fn parse_match_exists() { - let source = hex::decode("000000010000000a000000036b65790000000000").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::Exists - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_absent() { - let source = hex::decode("000000010000000a000000036b6579000000000e").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::Absent - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_equal() { - let source = - hex::decode("000000010000000a000000036b657900000000010000000576616c7565000000") - .unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::Equal(b"value".as_ref().into()) - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_contains() { - let source = - hex::decode("000000010000000a000000036b657900000000020000000576616c7565000000") - .unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::Contains(b"value".as_ref().into()) - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_begins_with() { - let source = - hex::decode("000000010000000a000000036b657900000000030000000576616c7565000000") - .unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::BeginsWith(b"value".as_ref().into()) - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_ends_with() { - let source = - hex::decode("000000010000000a000000036b657900000000040000000576616c7565000000") - .unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::EndsWith(b"value".as_ref().into()) - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_less_than() { - let source = - hex::decode("000000010000000a000000036b657900000000050000000576616c7565000000") - .unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::LessThan(b"value".as_ref().into()) - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_greater_than() { - let source = - hex::decode("000000010000000a000000036b657900000000060000000576616c7565000000") - .unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::GreaterThan(b"value".as_ref().into()) - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_less_than_equal() { - let source = - hex::decode("000000010000000a000000036b657900000000070000000576616c7565000000") - .unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::LessThanEqual(b"value".as_ref().into()) - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_greater_than_equal() { - let source = - hex::decode("000000010000000a000000036b657900000000080000000576616c7565000000") - .unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::GreaterThanEqual(b"value".as_ref().into()) - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_on() { - let source = - hex::decode("000000010000000a000000036b6579000000000900000000605fca30").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::On(chrono::Utc.timestamp(1616890416, 0)), - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_before() { - let source = - hex::decode("000000010000000a000000036b6579000000000a00000000605fca30").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::Before(chrono::Utc.timestamp(1616890416, 0)), - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_after() { - let source = - hex::decode("000000010000000a000000036b6579000000000b00000000605fca30").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::After(chrono::Utc.timestamp(1616890416, 0)), - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_on_or_before() { - let source = - hex::decode("000000010000000a000000036b6579000000000c00000000605fca30").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::OnOrBefore(chrono::Utc.timestamp(1616890416, 0)), - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } - - #[test] - fn parse_match_on_or_after() { - let source = - hex::decode("000000010000000a000000036b6579000000000d00000000605fca30").unwrap(); - - let (els, data) = CodeRequirements::parse_binary(&source).unwrap(); - - assert_eq!( - els, - CodeRequirements(vec![CodeRequirementExpression::InfoPlistKeyField( - "key".into(), - CodeRequirementMatchExpression::OnOrAfter(chrono::Utc.timestamp(1616890416, 0)), - )]) - ); - assert!(data.is_empty()); - verify_roundtrip(&els, &source); - } -} diff --git a/apple-codesign/src/code_resources.rs b/apple-codesign/src/code_resources.rs deleted file mode 100644 index c2eba2cb8..000000000 --- a/apple-codesign/src/code_resources.rs +++ /dev/null @@ -1,1481 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Functionality related to "code resources," external resources captured in signatures. -//! -//! Bundles can contain a `_CodeSignature/CodeResources` XML plist file -//! denoting signatures for resources not in the binary. The signature data -//! in the binary can record the digest of this file so integrity is transitively -//! verified. -//! -//! We've implemented our own (de)serialization code in this module because -//! the default derived Deserialize provided by the `plist` crate doesn't -//! handle enums correctly. We attempted to implement our own `Deserialize` -//! and `Visitor` traits to get things to parse, but we couldn't make it work. -//! We gave up and decided to just coerce the [plist::Value] instances instead. - -use { - crate::{ - bundle_signing::{BundleFileHandler, SignedMachOInfo}, - embedded_signature::DigestType, - error::AppleCodesignError, - }, - apple_bundles::{DirectoryBundle, DirectoryBundleFile}, - log::{debug, info, warn}, - plist::{Dictionary, Value}, - std::{ - cmp::Ordering, - collections::BTreeMap, - io::Write, - path::{Path, PathBuf}, - }, -}; - -#[derive(Clone, PartialEq)] -enum FilesValue { - Required(Vec), - Optional(Vec), -} - -impl std::fmt::Debug for FilesValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Required(digest) => f - .debug_struct("FilesValue") - .field("required", &true) - .field("digest", &hex::encode(digest)) - .finish(), - Self::Optional(digest) => f - .debug_struct("FilesValue") - .field("required", &false) - .field("digest", &hex::encode(digest)) - .finish(), - } - } -} - -impl std::fmt::Display for FilesValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Required(digest) => { - f.write_fmt(format_args!("{} (required)", hex::encode(digest))) - } - Self::Optional(digest) => { - f.write_fmt(format_args!("{} (optional)", hex::encode(digest))) - } - } - } -} - -impl TryFrom<&Value> for FilesValue { - type Error = AppleCodesignError; - - fn try_from(v: &Value) -> Result { - match v { - Value::Data(digest) => Ok(Self::Required(digest.to_vec())), - Value::Dictionary(dict) => { - let mut digest = None; - let mut optional = None; - - for (key, value) in dict.iter() { - match key.as_str() { - "hash" => { - let data = value.as_data().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expected for files entry, got {:?}", - value - )) - })?; - - digest = Some(data.to_vec()); - } - "optional" => { - let v = value.as_boolean().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expected boolean for optional key, got {:?}", - value - )) - })?; - - optional = Some(v); - } - key => { - return Err(AppleCodesignError::ResourcesPlistParse(format!( - "unexpected key in files dict: {}", - key - ))); - } - } - } - - match (digest, optional) { - (Some(digest), Some(true)) => Ok(Self::Optional(digest)), - (Some(digest), Some(false)) => Ok(Self::Required(digest)), - _ => Err(AppleCodesignError::ResourcesPlistParse( - "missing hash or optional key".to_string(), - )), - } - } - _ => Err(AppleCodesignError::ResourcesPlistParse(format!( - "bad value in files ; expected or , got {:?}", - v - ))), - } - } -} - -impl From<&FilesValue> for Value { - fn from(v: &FilesValue) -> Self { - match v { - FilesValue::Required(digest) => Self::Data(digest.to_vec()), - FilesValue::Optional(digest) => { - let mut dict = Dictionary::new(); - dict.insert("hash".to_string(), Value::Data(digest.to_vec())); - dict.insert("optional".to_string(), Value::Boolean(true)); - - Self::Dictionary(dict) - } - } - } -} - -#[derive(Clone, PartialEq)] -struct Files2Value { - cdhash: Option>, - hash: Option>, - hash2: Option>, - optional: Option, - requirement: Option, - symlink: Option, -} - -impl std::fmt::Debug for Files2Value { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Files2Value") - .field( - "cdhash", - &format_args!("{:?}", self.cdhash.as_ref().map(hex::encode)), - ) - .field( - "hash", - &format_args!("{:?}", self.hash.as_ref().map(hex::encode)), - ) - .field( - "hash2", - &format_args!("{:?}", self.hash2.as_ref().map(hex::encode)), - ) - .field("optional", &format_args!("{:?}", self.optional)) - .field("requirement", &format_args!("{:?}", self.requirement)) - .field("symlink", &format_args!("{:?}", self.symlink)) - .finish() - } -} - -impl TryFrom<&Value> for Files2Value { - type Error = AppleCodesignError; - - fn try_from(v: &Value) -> Result { - let dict = v.as_dictionary().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse("files2 value should be a dict".to_string()) - })?; - - let mut hash = None; - let mut hash2 = None; - let mut cdhash = None; - let mut optional = None; - let mut requirement = None; - let mut symlink = None; - - for (key, value) in dict.iter() { - match key.as_str() { - "cdhash" => { - let data = value.as_data().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expected for files2 cdhash entry, got {:?}", - value - )) - })?; - - cdhash = Some(data.to_vec()); - } - "hash" => { - let data = value.as_data().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expected for files2 hash entry, got {:?}", - value - )) - })?; - - hash = Some(data.to_vec()); - } - "hash2" => { - let data = value.as_data().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expected for files2 hash2 entry, got {:?}", - value - )) - })?; - - hash2 = Some(data.to_vec()); - } - "optional" => { - let v = value.as_boolean().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expected bool for optional key, got {:?}", - value - )) - })?; - - optional = Some(v); - } - "requirement" => { - let v = value.as_string().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expected string for requirement key, got {:?}", - value - )) - })?; - - requirement = Some(v.to_string()); - } - "symlink" => { - symlink = Some( - value - .as_string() - .ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expected string for symlink key, got {:?}", - value - )) - })? - .to_string(), - ); - } - key => { - return Err(AppleCodesignError::ResourcesPlistParse(format!( - "unexpected key in files2 dict entry: {}", - key - ))); - } - } - } - - Ok(Self { - cdhash, - hash, - hash2, - optional, - requirement, - symlink, - }) - } -} - -impl From<&Files2Value> for Value { - fn from(v: &Files2Value) -> Self { - let mut dict = Dictionary::new(); - - if let Some(cdhash) = &v.cdhash { - dict.insert("cdhash".to_string(), Value::Data(cdhash.to_vec())); - } - - if let Some(hash) = &v.hash { - dict.insert("hash".to_string(), Value::Data(hash.to_vec())); - } - - if let Some(hash2) = &v.hash2 { - dict.insert("hash2".to_string(), Value::Data(hash2.to_vec())); - } - - if let Some(optional) = &v.optional { - dict.insert("optional".to_string(), Value::Boolean(*optional)); - } - - if let Some(requirement) = &v.requirement { - dict.insert( - "requirement".to_string(), - Value::String(requirement.to_string()), - ); - } - - if let Some(symlink) = &v.symlink { - dict.insert("symlink".to_string(), Value::String(symlink.to_string())); - } - - Value::Dictionary(dict) - } -} - -#[derive(Clone, Debug, PartialEq)] -struct RulesValue { - omit: bool, - required: bool, - weight: Option, -} - -impl TryFrom<&Value> for RulesValue { - type Error = AppleCodesignError; - - fn try_from(v: &Value) -> Result { - match v { - Value::Boolean(true) => Ok(Self { - omit: false, - required: true, - weight: None, - }), - Value::Dictionary(dict) => { - let mut omit = None; - let mut optional = None; - let mut weight = None; - - for (key, value) in dict { - match key.as_str() { - "omit" => { - omit = Some(value.as_boolean().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "rules omit key value not a boolean; got {:?}", - value - )) - })?); - } - "optional" => { - optional = Some(value.as_boolean().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "rules optional key value not a boolean, got {:?}", - value - )) - })?); - } - "weight" => { - weight = Some(value.as_real().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "rules weight key value not a real, got {:?}", - value - )) - })?); - } - key => { - return Err(AppleCodesignError::ResourcesPlistParse(format!( - "extra key in rules dict: {}", - key - ))); - } - } - } - - Ok(Self { - omit: omit.unwrap_or(false), - required: !optional.unwrap_or(false), - weight, - }) - } - _ => Err(AppleCodesignError::ResourcesPlistParse( - "invalid value for rules entry".to_string(), - )), - } - } -} - -impl From<&RulesValue> for Value { - fn from(v: &RulesValue) -> Self { - if v.required && !v.omit && v.weight.is_none() { - Value::Boolean(true) - } else { - let mut dict = Dictionary::new(); - - if v.omit { - dict.insert("omit".to_string(), Value::Boolean(true)); - } - if !v.required { - dict.insert("optional".to_string(), Value::Boolean(true)); - } - - if let Some(weight) = v.weight { - dict.insert("weight".to_string(), Value::Real(weight)); - } - - Value::Dictionary(dict) - } - } -} - -#[derive(Clone, Debug, PartialEq)] -struct Rules2Value { - nested: Option, - omit: Option, - optional: Option, - weight: Option, -} - -impl TryFrom<&Value> for Rules2Value { - type Error = AppleCodesignError; - - fn try_from(v: &Value) -> Result { - let dict = v.as_dictionary().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse("rules2 value should be a dict".to_string()) - })?; - - let mut nested = None; - let mut omit = None; - let mut optional = None; - let mut weight = None; - - for (key, value) in dict.iter() { - match key.as_str() { - "nested" => { - nested = Some(value.as_boolean().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expected bool for rules2 nested key, got {:?}", - value - )) - })?); - } - "omit" => { - omit = Some(value.as_boolean().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expected bool for rules2 omit key, got {:?}", - value - )) - })?); - } - "optional" => { - optional = Some(value.as_boolean().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expected bool for rules2 optional key, got {:?}", - value - )) - })?); - } - "weight" => { - weight = Some(value.as_real().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expected real for rules2 weight key, got {:?}", - value - )) - })?); - } - key => { - return Err(AppleCodesignError::ResourcesPlistParse(format!( - "unexpected key in rules dict entry: {}", - key - ))); - } - } - } - - Ok(Self { - nested, - omit, - optional, - weight, - }) - } -} - -impl From<&Rules2Value> for Value { - fn from(v: &Rules2Value) -> Self { - let mut dict = Dictionary::new(); - - if let Some(true) = v.nested { - dict.insert("nested".to_string(), Value::Boolean(true)); - } - - if let Some(true) = v.omit { - dict.insert("omit".to_string(), Value::Boolean(true)); - } - - if let Some(true) = v.optional { - dict.insert("optional".to_string(), Value::Boolean(true)); - } - - if let Some(weight) = v.weight { - dict.insert("weight".to_string(), Value::Real(weight)); - } - - if dict.is_empty() { - Value::Boolean(true) - } else { - Value::Dictionary(dict) - } - } -} - -/// Represents an abstract rule in a `CodeResources` XML plist. -/// -/// This type represents both `` and `` entries. It contains a -/// superset of all fields for these entries. -#[derive(Clone, Debug)] -pub struct CodeResourcesRule { - /// The rule pattern. - /// - /// The `` in the `` or `` dict. - pub pattern: String, - - /// Whether this is an exclusion rule. - pub exclude: bool, - - pub nested: bool, - - pub omit: bool, - - /// Whether the rule is optional. - pub optional: bool, - - /// Weighting to apply to the rule. - pub weight: Option, - - re: regex::Regex, -} - -impl PartialEq for CodeResourcesRule { - fn eq(&self, other: &Self) -> bool { - self.pattern == other.pattern - && self.exclude == other.exclude - && self.nested == other.nested - && self.omit == other.omit - && self.optional == other.optional - && self.weight == other.weight - } -} - -impl Eq for CodeResourcesRule {} - -impl PartialOrd for CodeResourcesRule { - fn partial_cmp(&self, other: &Self) -> Option { - // Default weight is 1 if not specified. - let our_weight = self.weight.unwrap_or(1); - let their_weight = other.weight.unwrap_or(1); - - // Exclusion rules always take priority over inclusion rules. - // The smaller the weight, the less important it is. - match self.exclude.cmp(&other.exclude) { - Ordering::Equal => their_weight.partial_cmp(&our_weight), - Ordering::Greater => Some(Ordering::Less), - Ordering::Less => Some(Ordering::Greater), - } - } -} - -impl Ord for CodeResourcesRule { - fn cmp(&self, other: &Self) -> Ordering { - self.partial_cmp(other).unwrap() - } -} - -impl CodeResourcesRule { - pub fn new(pattern: impl ToString) -> Result { - Ok(Self { - pattern: pattern.to_string(), - exclude: false, - nested: false, - omit: false, - optional: false, - weight: None, - re: regex::Regex::new(&pattern.to_string()) - .map_err(|e| AppleCodesignError::ResourcesBadRegex(pattern.to_string(), e))?, - }) - } - - /// Mark this as an exclusion rule. - /// - /// Exclusion rules are internal to the builder and not materialized in the - /// `CodeResources` file. - #[must_use] - pub fn exclude(mut self) -> Self { - self.exclude = true; - self - } - - /// Mark the rule as nested. - #[must_use] - pub fn nested(mut self) -> Self { - self.nested = true; - self - } - - /// Set the omit field. - #[must_use] - pub fn omit(mut self) -> Self { - self.omit = true; - self - } - - /// Mark the files matched by this rule are optional. - #[must_use] - pub fn optional(mut self) -> Self { - self.optional = true; - self - } - - /// Set the weight of this rule. - #[must_use] - pub fn weight(mut self, v: u32) -> Self { - self.weight = Some(v); - self - } -} - -/// Which files section we are operating on and how to digest. -#[derive(Clone, Copy, Debug)] -pub enum FilesFlavor { - /// ``. - Rules, - /// ``. - Rules2, - /// `` and also include the SHA-1 digest. - Rules2WithSha1, -} - -/// Represents a `_CodeSignature/CodeResources` XML plist. -/// -/// This file/type represents a collection of file-based resources whose -/// content is digested and captured in this file. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct CodeResources { - files: BTreeMap, - files2: BTreeMap, - rules: BTreeMap, - rules2: BTreeMap, -} - -impl CodeResources { - /// Construct an instance by parsing an XML plist. - pub fn from_xml(xml: &[u8]) -> Result { - let plist = Value::from_reader_xml(xml).map_err(AppleCodesignError::ResourcesPlist)?; - - let dict = plist.into_dictionary().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse( - "plist root element should be a ".to_string(), - ) - })?; - - let mut files = BTreeMap::new(); - let mut files2 = BTreeMap::new(); - let mut rules = BTreeMap::new(); - let mut rules2 = BTreeMap::new(); - - for (key, value) in dict.iter() { - match key.as_ref() { - "files" => { - let dict = value.as_dictionary().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expecting files to be a dict, got {:?}", - value - )) - })?; - - for (key, value) in dict { - files.insert(key.to_string(), FilesValue::try_from(value)?); - } - } - "files2" => { - let dict = value.as_dictionary().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expecting files2 to be a dict, got {:?}", - value - )) - })?; - - for (key, value) in dict { - files2.insert(key.to_string(), Files2Value::try_from(value)?); - } - } - "rules" => { - let dict = value.as_dictionary().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expecting rules to be a dict, got {:?}", - value - )) - })?; - - for (key, value) in dict { - rules.insert(key.to_string(), RulesValue::try_from(value)?); - } - } - "rules2" => { - let dict = value.as_dictionary().ok_or_else(|| { - AppleCodesignError::ResourcesPlistParse(format!( - "expecting rules2 to be a dict, got {:?}", - value - )) - })?; - - for (key, value) in dict { - rules2.insert(key.to_string(), Rules2Value::try_from(value)?); - } - } - key => { - return Err(AppleCodesignError::ResourcesPlistParse(format!( - "unexpected key in root dict: {}", - key - ))); - } - } - } - - Ok(Self { - files, - files2, - rules, - rules2, - }) - } - - /// Serialize an instance to XML. - pub fn to_writer_xml(&self, mut writer: impl Write) -> Result<(), AppleCodesignError> { - let value = Value::from(self); - - // Ideally we'd write direct to the output. However, Apple's XML writer doesn't - // emit a space for empty elements. e.g. we do `` and Apple does ``. - // In addition, our writer doesn't emit a trailing newline. To make it easier to - // diff generated files with the canonical output, we normalize to Apple's format. - let mut data = Vec::::new(); - value - .to_writer_xml(&mut data) - .map_err(AppleCodesignError::ResourcesPlist)?; - - let data = String::from_utf8(data).expect("XML should be valid UTF-8"); - let data = data.replace("", ""); - let data = data.replace("", ""); - - writer.write_all(data.as_bytes())?; - writer.write_all(b"\n")?; - - Ok(()) - } - - /// Add a rule to this instance in the `` section. - pub fn add_rule(&mut self, rule: CodeResourcesRule) { - self.rules.insert( - rule.pattern, - RulesValue { - omit: rule.omit, - required: !rule.optional, - weight: rule.weight.map(|x| x as f64), - }, - ); - } - - /// Add a rule to this instance in the `` section. - pub fn add_rule2(&mut self, rule: CodeResourcesRule) { - self.rules2.insert( - rule.pattern, - Rules2Value { - nested: if rule.nested { Some(true) } else { None }, - omit: if rule.omit { Some(true) } else { None }, - optional: if rule.optional { Some(true) } else { None }, - weight: rule.weight.map(|x| x as f64), - }, - ); - } - - /// Seal a regular file. - /// - /// This will digest the content specified and record that digest in the files or - /// files2 list. - /// - /// To seal a symlink, call [CodeResources::seal_symlink] instead. If the file - /// is a Mach-O file, call [CodeResources::seal_macho] instead. - pub fn seal_regular_file( - &mut self, - files_flavor: FilesFlavor, - path: impl ToString, - content: impl AsRef<[u8]>, - optional: bool, - ) -> Result<(), AppleCodesignError> { - match files_flavor { - FilesFlavor::Rules => { - let digest = DigestType::Sha1.digest_data(content.as_ref())?; - self.files.insert( - path.to_string(), - if optional { - FilesValue::Optional(digest) - } else { - FilesValue::Required(digest) - }, - ); - - Ok(()) - } - FilesFlavor::Rules2 => { - let hash2 = Some(DigestType::Sha256.digest_data(content.as_ref())?); - - self.files2.insert( - path.to_string(), - Files2Value { - cdhash: None, - hash: None, - hash2, - optional: if optional { Some(true) } else { None }, - requirement: None, - symlink: None, - }, - ); - - Ok(()) - } - FilesFlavor::Rules2WithSha1 => { - let hash = Some(DigestType::Sha1.digest_data(content.as_ref())?); - let hash2 = Some(DigestType::Sha256.digest_data(content.as_ref())?); - - self.files2.insert( - path.to_string(), - Files2Value { - cdhash: None, - hash, - hash2, - optional: if optional { Some(true) } else { None }, - requirement: None, - symlink: None, - }, - ); - - Ok(()) - } - } - } - - /// Seal a symlink file. - /// - /// `path` is the path of the symlink and `target` is the path it points to. - pub fn seal_symlink(&mut self, path: impl ToString, target: impl ToString) { - self.files2.insert( - path.to_string(), - Files2Value { - cdhash: None, - hash: None, - hash2: None, - optional: None, - requirement: None, - symlink: Some(target.to_string()), - }, - ); - } - - /// Record metadata of a previously signed Mach-O binary. - /// - /// If sealing a fat/universal binary, pass in metadata for the first Mach-O within in. - pub fn seal_macho( - &mut self, - path: impl ToString, - info: &SignedMachOInfo, - optional: bool, - ) -> Result<(), AppleCodesignError> { - self.files2.insert( - path.to_string(), - Files2Value { - cdhash: Some(DigestType::Sha256Truncated.digest_data(&info.code_directory_blob)?), - hash: None, - hash2: None, - optional: if optional { Some(true) } else { None }, - requirement: info.designated_code_requirement.clone(), - symlink: None, - }, - ); - - Ok(()) - } -} - -impl From<&CodeResources> for Value { - fn from(cr: &CodeResources) -> Self { - let mut dict = Dictionary::new(); - - dict.insert( - "files".to_string(), - Value::Dictionary( - cr.files - .iter() - .map(|(key, value)| (key.to_string(), Value::from(value))) - .collect::(), - ), - ); - - dict.insert( - "files2".to_string(), - Value::Dictionary( - cr.files2 - .iter() - .map(|(key, value)| (key.to_string(), Value::from(value))) - .collect::(), - ), - ); - - if !cr.rules.is_empty() { - dict.insert( - "rules".to_string(), - Value::Dictionary( - cr.rules - .iter() - .map(|(key, value)| (key.to_string(), Value::from(value))) - .collect::(), - ), - ); - } - - if !cr.rules2.is_empty() { - dict.insert( - "rules2".to_string(), - Value::Dictionary( - cr.rules2 - .iter() - .map(|(key, value)| (key.to_string(), Value::from(value))) - .collect::(), - ), - ); - } - - Value::Dictionary(dict) - } -} - -#[derive(Clone, Debug)] -enum RulesEvaluation { - /// File should be ignored completely. - Exclude, - - /// File isn't sealed but it is installed. - Omit, - - /// Seal a symlink. - /// - /// Members are the relative path and the target path. - SealSymlink(String, String), - - /// Seal a nested Mach-O binary. - /// - /// Members are the relative path and whether the rule is optional. - SealNested(String, bool), - - /// Seal a regular file. - /// - /// Members are the relative path and whether the rule is optional. - SealRegularFile(String, bool), - - /// File doesn't match any rules. - NoRule, -} - -/// Interface for constructing a `CodeResources` instance. -/// -/// This type is used during bundle signing to construct a `CodeResources` instance. -/// It contains logic for validating a file against registered processing rules and -/// handling it accordingly. -#[derive(Clone, Debug)] -pub struct CodeResourcesBuilder { - rules: Vec, - rules2: Vec, - resources: CodeResources, - digests: Vec, -} - -impl Default for CodeResourcesBuilder { - fn default() -> Self { - Self { - rules: vec![], - rules2: vec![], - resources: CodeResources::default(), - digests: vec![DigestType::Sha256], - } - } -} - -impl CodeResourcesBuilder { - /// Obtain an instance with default rules for a bundle with a `Resources/` directory. - pub fn default_resources_rules() -> Result { - let mut slf = Self::default(); - - slf.add_rule(CodeResourcesRule::new("^version.plist$")?); - slf.add_rule(CodeResourcesRule::new("^Resources/")?); - slf.add_rule( - CodeResourcesRule::new("^Resources/.*\\.lproj/")? - .optional() - .weight(1000), - ); - slf.add_rule(CodeResourcesRule::new("^Resources/Base\\.lproj/")?.weight(1010)); - slf.add_rule( - CodeResourcesRule::new("^Resources/.*\\.lproj/locversion.plist$")? - .omit() - .weight(1100), - ); - - slf.add_rule2(CodeResourcesRule::new("^.*")?); - slf.add_rule2(CodeResourcesRule::new("^[^/]+$")?.nested().weight(10)); - slf.add_rule2(CodeResourcesRule::new("^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/")? - .nested().weight(10)); - slf.add_rule2(CodeResourcesRule::new(".*\\.dSYM($|/)")?.weight(11)); - slf.add_rule2( - CodeResourcesRule::new("^(.*/)?\\.DS_Store$")? - .omit() - .weight(2000), - ); - slf.add_rule2(CodeResourcesRule::new("^Info\\.plist$")?.omit().weight(20)); - slf.add_rule2(CodeResourcesRule::new("^version\\.plist$")?.weight(20)); - slf.add_rule2(CodeResourcesRule::new("^embedded\\.provisionprofile$")?.weight(20)); - slf.add_rule2(CodeResourcesRule::new("^PkgInfo$")?.omit().weight(20)); - slf.add_rule2(CodeResourcesRule::new("^Resources/")?.weight(20)); - slf.add_rule2( - CodeResourcesRule::new("^Resources/.*\\.lproj/")? - .optional() - .weight(1000), - ); - slf.add_rule2(CodeResourcesRule::new("^Resources/Base\\.lproj/")?.weight(1010)); - slf.add_rule2( - CodeResourcesRule::new("^Resources/.*\\.lproj/locversion.plist$")? - .omit() - .weight(1100), - ); - - Ok(slf) - } - - /// Obtain an instance with default rules for a bundle without a `Resources/` directory. - pub fn default_no_resources_rules() -> Result { - let mut slf = Self::default(); - - slf.add_rule(CodeResourcesRule::new("^version.plist$")?); - slf.add_rule(CodeResourcesRule::new("^.*")?); - slf.add_rule( - CodeResourcesRule::new("^.*\\.lproj")? - .optional() - .weight(1000), - ); - slf.add_rule(CodeResourcesRule::new("^Base\\.lproj")?.weight(1010)); - slf.add_rule( - CodeResourcesRule::new("^.*\\.lproj/locversion.plist$")? - .omit() - .weight(1100), - ); - slf.add_rule2(CodeResourcesRule::new("^.*")?); - slf.add_rule2(CodeResourcesRule::new(".*\\.dSYM($|/)")?.weight(11)); - slf.add_rule2( - CodeResourcesRule::new("^(.*/)?\\.DS_Store$")? - .omit() - .weight(2000), - ); - slf.add_rule2(CodeResourcesRule::new("^Info\\.plist$")?.omit().weight(20)); - slf.add_rule2(CodeResourcesRule::new("^version\\.plist$")?.weight(20)); - slf.add_rule2(CodeResourcesRule::new("^embedded\\.provisionprofile$")?.weight(20)); - slf.add_rule2(CodeResourcesRule::new("^PkgInfo$")?.omit().weight(20)); - slf.add_rule2( - CodeResourcesRule::new("^.*\\.lproj/")? - .optional() - .weight(1000), - ); - slf.add_rule2(CodeResourcesRule::new("^Base\\.lproj")?.weight(1010)); - slf.add_rule2( - CodeResourcesRule::new("^.*\\.lproj/locversion.plist$")? - .omit() - .weight(1100), - ); - - Ok(slf) - } - - /// Set the digests to record in this instance. - pub fn set_digests(&mut self, digests: impl Iterator) { - self.digests = digests.collect::>(); - } - - /// Add a rule to this instance in the `` section. - pub fn add_rule(&mut self, rule: CodeResourcesRule) { - self.rules.push(rule.clone()); - self.rules.sort(); - self.resources.add_rule(rule); - } - - /// Add a rule to this instance in the `` section. - pub fn add_rule2(&mut self, rule: CodeResourcesRule) { - self.rules2.push(rule.clone()); - self.rules2.sort(); - self.resources.add_rule2(rule); - } - - /// Add an exclusion rule to the processing rules. - /// - /// Exclusion rules are not added to the [CodeResources] because they are - /// for building only. - pub fn add_exclusion_rule(&mut self, rule: CodeResourcesRule) { - self.rules.push(rule.clone()); - self.rules.sort(); - self.rules2.push(rule); - self.rules2.sort(); - } - - /// Find the first rule matching a given path. - /// - /// Rule processing is a bit complicated. Internally, rules are sorted by - /// decreasing priority. So the first pattern that matches is the rule we use. - /// However, there are a few special cases. - /// - /// If a path begins with `Contents/`, that prefix is ignored when performing the - /// pattern match. - /// - /// Directories are special. If an exclusion rule matches a directory, that directory - /// tree should be ignored. There are also default rules for handling nested bundles. - /// These rules take precedence over directory exclusion rules. - fn find_rule(rules: &[CodeResourcesRule], path: &str) -> Option { - let parts = path.split('/').collect::>(); - - let mut exclude_override = false; - - let rule = rules.iter().find(|rule| { - // Nested rules matching leaf-most directory with `.` result in match. - // But we treat as exclusion, as these are treated as nested bundles, - // which are handled externally. - if rule.nested { - for last_part in 1..parts.len() - 1 { - let parent = parts[0..last_part].join("/"); - - if rule.re.is_match(&parent) && parts[last_part - 1].contains('.') { - exclude_override = true; - return true; - } - } - } - - // Directory exclusions match entire directory tree. So walk the parents and yield - // this rule if matches. - if rule.exclude { - for last_part in 1..parts.len() - 1 { - let parent = parts[0..last_part].join("/"); - - if rule.re.is_match(&parent) { - return true; - } - } - } - - rule.re.is_match(path) - }); - - if let Some(rule) = rule { - let mut rule = rule.clone(); - - if exclude_override { - rule.exclude = true; - } - - Some(rule) - } else { - None - } - } - - fn evaluate_rules( - rules: &[CodeResourcesRule], - relative_path: impl AsRef, - symlink_target: Option, - ) -> Result { - // Always use UNIX style directory separators. - let relative_path = relative_path.as_ref().to_string_lossy().replace('\\', "/"); - - // The Contents/ prefix is also removed for pattern matching and references in the - // resources file. - let relative_path = relative_path - .strip_prefix("Contents/") - .unwrap_or(&relative_path) - .to_string(); - - match Self::find_rule(rules, relative_path.as_ref()) { - Some(rule) => { - debug!( - "{} matches {} rule {}", - relative_path, - if rule.exclude || rule.omit { - "exclusion" - } else { - "inclusion" - }, - rule.pattern - ); - - if rule.exclude { - Ok(RulesEvaluation::Exclude) - } else if rule.omit { - Ok(RulesEvaluation::Omit) - } else if rule.nested && symlink_target.is_some() { - // Symlinks in nested bundles can be excluded since they should have - // been processed by the nested bundle. - Ok(RulesEvaluation::Exclude) - } else if let Some(target) = symlink_target { - let target = target.to_string_lossy().replace('\\', "/"); - - Ok(RulesEvaluation::SealSymlink(relative_path, target)) - } else if rule.nested { - Ok(RulesEvaluation::SealNested(relative_path, rule.optional)) - } else { - Ok(RulesEvaluation::SealRegularFile( - relative_path, - rule.optional, - )) - } - } - None => { - debug!("{} doesn't match any rule", relative_path); - Ok(RulesEvaluation::NoRule) - } - } - } - - /// Process the `` set for a given file. - fn process_file_rules2( - &mut self, - file: &DirectoryBundleFile, - file_handler: &dyn BundleFileHandler, - ) -> Result<(), AppleCodesignError> { - match Self::evaluate_rules( - &self.rules2, - file.relative_path(), - file.symlink_target() - .map_err(AppleCodesignError::DirectoryBundle)?, - )? { - RulesEvaluation::Exclude => { - // Excluded files are hard ignored. These files are likely handled out-of-band - // from this builder. - Ok(()) - } - RulesEvaluation::Omit => { - // Omitted files aren't sealed. But they are installed. - file_handler.install_file(file) - } - RulesEvaluation::NoRule => { - // No rule match is assumed to mean full ignore. - Ok(()) - } - RulesEvaluation::SealSymlink(relative_path, target) => { - info!("sealing symlink {} -> {}", relative_path, target); - self.resources.seal_symlink(relative_path, target); - file_handler.install_file(file) - } - RulesEvaluation::SealNested(relative_path, optional) => { - // The assumption that a nested match means Mach-O may not be correct. - info!("sealing Mach-O file {}", relative_path); - let macho_info = file_handler.sign_and_install_macho(file)?; - - self.resources - .seal_macho(relative_path, &macho_info, optional) - } - RulesEvaluation::SealRegularFile(relative_path, optional) => { - info!("sealing regular file {}", relative_path); - let data = std::fs::read(file.absolute_path())?; - - let flavor = if self.digests.contains(&DigestType::Sha1) { - FilesFlavor::Rules2WithSha1 - } else { - FilesFlavor::Rules2 - }; - - self.resources - .seal_regular_file(flavor, relative_path, data, optional)?; - file_handler.install_file(file) - } - } - } - - /// Process the `` set for a given file. - /// - /// Since `` handling actually does the file installs, the only role of this - /// handler is to record the SHA-1 seals in ``. Keep in mind that `` can't - /// handle symlinks or nested Mach-O binaries. So we only care about regular files here. - fn process_file_rules(&mut self, file: &DirectoryBundleFile) -> Result<(), AppleCodesignError> { - match Self::evaluate_rules( - &self.rules, - file.relative_path(), - file.symlink_target() - .map_err(AppleCodesignError::DirectoryBundle)?, - )? { - RulesEvaluation::Exclude - | RulesEvaluation::Omit - | RulesEvaluation::NoRule - | RulesEvaluation::SealSymlink(..) - | RulesEvaluation::SealNested(..) => Ok(()), - RulesEvaluation::SealRegularFile(relative_path, optional) => { - let data = std::fs::read(file.absolute_path())?; - - self.resources - .seal_regular_file(FilesFlavor::Rules, relative_path, data, optional) - } - } - } - - /// Process a file for resource handling. - /// - /// This determines whether a file is relevant for inclusion in the CodeResources - /// file and takes actions to process it, if necessary. - pub fn process_file( - &mut self, - file: &DirectoryBundleFile, - file_handler: &dyn BundleFileHandler, - ) -> Result<(), AppleCodesignError> { - self.process_file_rules2(file, file_handler)?; - self.process_file_rules(file) - } - - /// Process a nested bundle for inclusion in resource handling. - /// - /// This will attempt to seal the main digest of the bundle into this resources file. - pub fn process_nested_bundle( - &mut self, - relative_path: &str, - bundle: &DirectoryBundle, - ) -> Result<(), AppleCodesignError> { - let main_exe = match bundle - .files(false) - .map_err(AppleCodesignError::DirectoryBundle)? - .into_iter() - .find(|file| matches!(file.is_main_executable(), Ok(true))) - { - Some(path) => path, - None => { - warn!( - "nested bundle at {} does not have main executable; nothing to seal", - relative_path - ); - return Ok(()); - } - }; - - let (relative_path, optional) = - match Self::evaluate_rules(&self.rules2, relative_path, None)? { - RulesEvaluation::SealRegularFile(relative_path, optional) => { - (relative_path, optional) - } - RulesEvaluation::SealNested(relative_path, optional) => (relative_path, optional), - RulesEvaluation::Exclude => { - info!( - "excluding signing nested bundle {} because of matched resources rule", - relative_path - ); - return Ok(()); - } - res => { - warn!( - "unexpected resource rules evaluation result for nested bundle {}: {:?}", - relative_path, res - ); - return Err(AppleCodesignError::BundleUnexpectedResourceRuleResult); - } - }; - - let macho_data = std::fs::read(main_exe.absolute_path())?; - let macho_info = SignedMachOInfo::parse_data(&macho_data)?; - - info!("sealing nested bundle at {}", relative_path); - self.resources - .seal_macho(relative_path, &macho_info, optional)?; - - Ok(()) - } - - /// Write CodeResources XML content to a writer. - pub fn write_code_resources(&self, writer: impl Write) -> Result<(), AppleCodesignError> { - self.resources.to_writer_xml(writer) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const FIREFOX_SNIPPET: &str = r#" - - - - - files - - Resources/XUL.sig - Y0SEPxyC6hCQ+rl4LTRmXy7F9DQ= - Resources/en.lproj/InfoPlist.strings - - hash - U8LTYe+cVqPcBu9aLvcyyfp+dAg= - optional - - - Resources/firefox-bin.sig - ZvZ3yDciAF4kB9F06Xr3gKi3DD4= - - files2 - - Library/LaunchServices/org.mozilla.updater - - hash2 - iMnDHpWkKTI6xLi9Av93eNuIhxXhv3C18D4fljCfw2Y= - - TestOptional - - hash2 - iMnDHpWkKTI6xLi9Av93eNuIhxXhv3C18D4fljCfw2Y= - optional - - - MacOS/XUL - - cdhash - NevNMzQBub9OjomMUAk2xBumyHM= - requirement - 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" - - MacOS/SafariForWebKitDevelopment - - symlink - /Library/Application Support/Apple/Safari/SafariForWebKitDevelopment - - - rules - - ^Resources/ - - ^Resources/.*\.lproj/ - - optional - - weight - 1000 - - - rules2 - - .*\.dSYM($|/) - - weight - 11 - - ^(.*/)?\.DS_Store$ - - omit - - weight - 2000 - - ^[^/]+$ - - nested - - weight - 10 - - optional - - optional - - - - - "#; - - #[test] - fn parse_firefox() { - let resources = CodeResources::from_xml(FIREFOX_SNIPPET.as_bytes()).unwrap(); - - // Serialize back to XML. - let mut buffer = Vec::::new(); - resources.to_writer_xml(&mut buffer).unwrap(); - let resources2 = CodeResources::from_xml(&buffer).unwrap(); - - assert_eq!(resources, resources2); - } -} diff --git a/apple-codesign/src/cryptography.rs b/apple-codesign/src/cryptography.rs deleted file mode 100644 index c74aba537..000000000 --- a/apple-codesign/src/cryptography.rs +++ /dev/null @@ -1,744 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Common cryptography primitives. - -use { - crate::{ - remote_signing::{session_negotiation::PublicKeyPeerDecrypt, RemoteSignError}, - AppleCodesignError, - }, - bytes::Bytes, - der::{asn1, Decode, Document, Encode, SecretDocument}, - elliptic_curve::{ - sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint}, - AffinePoint, Curve, FieldSize, ProjectiveArithmetic, SecretKey as ECSecretKey, - }, - oid_registry::{ - OID_EC_P256, OID_KEY_TYPE_EC_PUBLIC_KEY, OID_PKCS1_RSAENCRYPTION, OID_SIG_ED25519, - }, - p256::NistP256, - pkcs1::RsaPrivateKey, - pkcs8::{AlgorithmIdentifier, EncodePrivateKey, ObjectIdentifier, PrivateKeyInfo}, - ring::signature::{EcdsaKeyPair, Ed25519KeyPair, KeyPair, RsaKeyPair}, - rsa::{ - algorithms::mgf1_xor, pkcs1::DecodeRsaPrivateKey, BigUint, PaddingScheme, - RsaPrivateKey as RsaConstructedKey, - }, - signature::Signer, - subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}, - x509_certificate::{ - CapturedX509Certificate, EcdsaCurve, InMemorySigningKeyPair, KeyAlgorithm, KeyInfoSigner, - Sign, Signature, SignatureAlgorithm, X509CertificateError, - }, - zeroize::Zeroizing, -}; - -/// A supertrait generically describing a private key capable of signing and possibly decryption. -pub trait PrivateKey: KeyInfoSigner { - fn as_key_info_signer(&self) -> &dyn KeyInfoSigner; - - fn to_public_key_peer_decrypt( - &self, - ) -> Result, AppleCodesignError>; - - /// Signals the end of operations on the private key. - /// - /// Implementations can use this to do things like destroy private key matter, disconnect - /// from a hardware device, etc. - fn finish(&self) -> Result<(), AppleCodesignError>; -} - -#[derive(Clone, Debug)] -pub struct InMemoryRsaKey { - // Validated at construction time to be DER for an RsaPrivateKey. - private_key: SecretDocument, -} - -impl InMemoryRsaKey { - /// Construct a new instance from DER data, validating DER in process. - fn from_der(der_data: &[u8]) -> Result { - RsaPrivateKey::from_der(der_data)?; - - let private_key = Document::from_der(der_data)?.into_secret(); - - Ok(Self { private_key }) - } - - fn rsa_private_key(&self) -> RsaPrivateKey<'_> { - RsaPrivateKey::from_der(self.private_key.as_bytes()) - .expect("internal content should be PKCS#1 DER private key data") - } -} - -impl From<&InMemoryRsaKey> for RsaConstructedKey { - fn from(key: &InMemoryRsaKey) -> Self { - let key = key.rsa_private_key(); - - let n = BigUint::from_bytes_be(key.modulus.as_bytes()); - let e = BigUint::from_bytes_be(key.public_exponent.as_bytes()); - let d = BigUint::from_bytes_be(key.private_exponent.as_bytes()); - let prime1 = BigUint::from_bytes_be(key.prime1.as_bytes()); - let prime2 = BigUint::from_bytes_be(key.prime2.as_bytes()); - let primes = vec![prime1, prime2]; - - Self::from_components(n, e, d, primes) - } -} - -impl TryFrom for InMemorySigningKeyPair { - type Error = AppleCodesignError; - - fn try_from(value: InMemoryRsaKey) -> Result { - let key_pair = RsaKeyPair::from_der(value.private_key.as_bytes()).map_err(|e| { - AppleCodesignError::CertificateGeneric(format!( - "error importing RSA key to ring: {}", - e - )) - })?; - - Ok(InMemorySigningKeyPair::Rsa( - key_pair, - value.private_key.as_bytes().to_vec(), - )) - } -} - -impl EncodePrivateKey for InMemoryRsaKey { - fn to_pkcs8_der(&self) -> pkcs8::Result { - let raw = PrivateKeyInfo::new(pkcs1::ALGORITHM_ID, self.private_key.as_bytes()).to_vec()?; - - Ok(Document::from_der(&raw)?.into_secret()) - } -} - -impl PublicKeyPeerDecrypt for InMemoryRsaKey { - fn decrypt(&self, ciphertext: &[u8]) -> Result, RemoteSignError> { - let key = RsaConstructedKey::from_pkcs1_der(self.private_key.as_bytes()) - .map_err(|e| RemoteSignError::Crypto(format!("failed to parse RSA key: {}", e)))?; - - let padding = PaddingScheme::new_oaep::(); - - let plaintext = key - .decrypt(padding, ciphertext) - .map_err(|e| RemoteSignError::Crypto(format!("RSA decryption failure: {}", e)))?; - - Ok(plaintext) - } -} - -#[derive(Clone, Debug)] -pub struct InMemoryEcdsaKey -where - C: Curve + ProjectiveArithmetic, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldSize: ModulusSize, -{ - curve: ObjectIdentifier, - secret_key: ECSecretKey, -} - -impl<'a, C> InMemoryEcdsaKey -where - C: Curve + ProjectiveArithmetic, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldSize: ModulusSize, -{ - pub fn curve(&self) -> Result { - match self.curve.as_bytes() { - x if x == OID_EC_P256.as_bytes() => Ok(EcdsaCurve::Secp256r1), - _ => Err(AppleCodesignError::CertificateGeneric(format!( - "unknown ECDSA curve: {}", - self.curve - ))), - } - } -} - -impl TryFrom> for InMemorySigningKeyPair -where - C: Curve + ProjectiveArithmetic, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldSize: ModulusSize, -{ - type Error = AppleCodesignError; - - fn try_from(key: InMemoryEcdsaKey) -> Result { - let curve = key.curve()?; - - let private_key = key.secret_key.to_be_bytes(); - let public_key = key.secret_key.public_key().to_encoded_point(false); - - let key_pair = EcdsaKeyPair::from_private_key_and_public_key( - curve.into(), - private_key.as_ref(), - public_key.as_bytes(), - ) - .map_err(|e| { - AppleCodesignError::CertificateGeneric(format!( - "unable to convert ECDSA private key: {}", - e - )) - })?; - - Ok(Self::Ecdsa(key_pair, curve, vec![])) - } -} - -impl EncodePrivateKey for InMemoryEcdsaKey -where - C: Curve + ProjectiveArithmetic, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldSize: ModulusSize, -{ - fn to_pkcs8_der(&self) -> pkcs8::Result { - let private_key = self.secret_key.to_sec1_der()?; - - PrivateKeyInfo { - algorithm: AlgorithmIdentifier { - oid: ObjectIdentifier::from_bytes(OID_KEY_TYPE_EC_PUBLIC_KEY.as_bytes()) - .expect("OID construction should work"), - parameters: Some(asn1::AnyRef::from(&self.curve)), - }, - private_key: private_key.as_ref(), - public_key: None, - } - .try_into() - } -} - -impl PublicKeyPeerDecrypt for InMemoryEcdsaKey -where - C: Curve + ProjectiveArithmetic, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldSize: ModulusSize, -{ - fn decrypt(&self, _ciphertext: &[u8]) -> Result, RemoteSignError> { - Err(RemoteSignError::Crypto( - "decryption using ECDSA keys is not yet implemented".into(), - )) - } -} - -#[derive(Clone, Debug)] -pub struct InMemoryEd25519Key { - private_key: Zeroizing>, -} - -impl TryFrom for InMemorySigningKeyPair { - type Error = AppleCodesignError; - - fn try_from(key: InMemoryEd25519Key) -> Result { - let key_pair = - Ed25519KeyPair::from_seed_unchecked(key.private_key.as_ref()).map_err(|e| { - AppleCodesignError::CertificateGeneric(format!( - "unable to convert ED25519 private key: {}", - e - )) - })?; - - Ok(Self::Ed25519(key_pair)) - } -} - -impl EncodePrivateKey for InMemoryEd25519Key { - fn to_pkcs8_der(&self) -> pkcs8::Result { - let algorithm = AlgorithmIdentifier { - oid: ObjectIdentifier::from_bytes(OID_SIG_ED25519.as_bytes()).expect("OID is valid"), - parameters: None, - }; - - let key_ref: &[u8] = self.private_key.as_ref(); - let value = Zeroizing::new(asn1::OctetString::new(key_ref)?.to_vec()?); - - PrivateKeyInfo::new(algorithm, value.as_ref()).try_into() - } -} - -impl PublicKeyPeerDecrypt for InMemoryEd25519Key { - fn decrypt(&self, _ciphertext: &[u8]) -> Result, RemoteSignError> { - Err(RemoteSignError::Crypto( - "decryption using ED25519 keys is not yet implemented".into(), - )) - } -} - -/// Holds a private key in memory. -#[derive(Clone, Debug)] -pub enum InMemoryPrivateKey { - /// ECDSA private key using Nist P256 curve. - EcdsaP256(InMemoryEcdsaKey), - /// ED25519 private key. - Ed25519(InMemoryEd25519Key), - /// RSA private key. - Rsa(InMemoryRsaKey), -} - -impl<'a> TryFrom> for InMemoryPrivateKey { - type Error = pkcs8::Error; - - fn try_from(value: PrivateKeyInfo<'a>) -> Result { - match value.algorithm.oid { - x if x.as_bytes() == OID_PKCS1_RSAENCRYPTION.as_bytes() => { - Ok(Self::Rsa(InMemoryRsaKey::from_der(value.private_key)?)) - } - x if x.as_bytes() == OID_KEY_TYPE_EC_PUBLIC_KEY.as_bytes() => { - let curve_oid = value.algorithm.parameters_oid()?; - - match curve_oid.as_bytes() { - x if x == OID_EC_P256.as_bytes() => { - let secret_key = ECSecretKey::::try_from(value)?; - - Ok(Self::EcdsaP256(InMemoryEcdsaKey { - curve: curve_oid, - secret_key, - })) - } - _ => Err(pkcs8::Error::ParametersMalformed), - } - } - x if x.as_bytes() == OID_SIG_ED25519.as_bytes() => { - // The private key seed should start at byte offset 2. - Ok(Self::Ed25519(InMemoryEd25519Key { - private_key: Zeroizing::new((&value.private_key[2..]).to_vec()), - })) - } - _ => Err(pkcs8::Error::KeyMalformed), - } - } -} - -impl TryFrom for InMemorySigningKeyPair { - type Error = AppleCodesignError; - - fn try_from(key: InMemoryPrivateKey) -> Result { - match key { - InMemoryPrivateKey::Rsa(key) => key.try_into(), - InMemoryPrivateKey::EcdsaP256(key) => key.try_into(), - InMemoryPrivateKey::Ed25519(key) => key.try_into(), - } - } -} - -impl EncodePrivateKey for InMemoryPrivateKey { - fn to_pkcs8_der(&self) -> pkcs8::Result { - match self { - Self::EcdsaP256(key) => key.to_pkcs8_der(), - Self::Ed25519(key) => key.to_pkcs8_der(), - Self::Rsa(key) => key.to_pkcs8_der(), - } - } -} - -impl Signer for InMemoryPrivateKey { - fn try_sign(&self, msg: &[u8]) -> Result { - let key_pair = InMemorySigningKeyPair::try_from(self.clone()) - .map_err(signature::Error::from_source)?; - - key_pair.try_sign(msg) - } -} - -impl Sign for InMemoryPrivateKey { - fn sign(&self, message: &[u8]) -> Result<(Vec, SignatureAlgorithm), X509CertificateError> { - let algorithm = self.signature_algorithm()?; - - Ok((self.try_sign(message)?.into(), algorithm)) - } - - fn key_algorithm(&self) -> Option { - Some(match self { - Self::EcdsaP256(_) => KeyAlgorithm::Ecdsa(EcdsaCurve::Secp256r1), - Self::Ed25519(_) => KeyAlgorithm::Ed25519, - Self::Rsa(_) => KeyAlgorithm::Rsa, - }) - } - - fn public_key_data(&self) -> Bytes { - match self { - Self::EcdsaP256(key) => Bytes::copy_from_slice( - key.secret_key - .public_key() - .to_encoded_point(false) - .as_bytes(), - ), - Self::Ed25519(key) => { - if let Ok(key) = Ed25519KeyPair::from_seed_unchecked(key.private_key.as_ref()) { - Bytes::copy_from_slice(key.public_key().as_ref()) - } else { - Bytes::new() - } - } - Self::Rsa(key) => { - let key = key.rsa_private_key(); - - Bytes::copy_from_slice( - key.public_key() - .to_vec() - .expect("RSA public key DER encoding should not fail") - .as_ref(), - ) - } - } - } - - fn signature_algorithm(&self) -> Result { - Ok(match self { - Self::EcdsaP256(_) => SignatureAlgorithm::EcdsaSha256, - Self::Ed25519(_) => SignatureAlgorithm::Ed25519, - Self::Rsa(_) => SignatureAlgorithm::RsaSha256, - }) - } - - fn private_key_data(&self) -> Option> { - match self { - Self::EcdsaP256(key) => Some(key.secret_key.to_be_bytes().to_vec()), - Self::Ed25519(key) => Some((*key.private_key).clone()), - Self::Rsa(key) => Some(key.private_key.as_bytes().to_vec()), - } - } - - fn rsa_primes(&self) -> Result, Vec)>, X509CertificateError> { - if let Self::Rsa(key) = self { - let key = key.rsa_private_key(); - - Ok(Some(( - key.prime1.as_bytes().to_vec(), - key.prime2.as_bytes().to_vec(), - ))) - } else { - Ok(None) - } - } -} - -impl KeyInfoSigner for InMemoryPrivateKey {} - -impl PublicKeyPeerDecrypt for InMemoryPrivateKey { - fn decrypt(&self, ciphertext: &[u8]) -> Result, RemoteSignError> { - match self { - Self::Rsa(key) => key.decrypt(ciphertext), - Self::EcdsaP256(key) => key.decrypt(ciphertext), - Self::Ed25519(key) => key.decrypt(ciphertext), - } - } -} - -impl PrivateKey for InMemoryPrivateKey { - fn as_key_info_signer(&self) -> &dyn KeyInfoSigner { - self - } - - fn to_public_key_peer_decrypt( - &self, - ) -> Result, AppleCodesignError> { - Ok(Box::new(self.clone())) - } - - fn finish(&self) -> Result<(), AppleCodesignError> { - Ok(()) - } -} - -impl InMemoryPrivateKey { - /// Construct an instance by parsing PKCS#8 DER data. - pub fn from_pkcs8_der(data: impl AsRef<[u8]>) -> Result { - let pki = PrivateKeyInfo::try_from(data.as_ref()).map_err(|e| { - AppleCodesignError::CertificateGeneric(format!("when parsing PKCS#8 data: {}", e)) - })?; - - pki.try_into().map_err(|e| { - AppleCodesignError::CertificateGeneric(format!( - "when converting parsed PKCS#8 to a private key: {}", - e - )) - }) - } -} - -fn bmp_string(s: &str) -> Vec { - let utf16: Vec = s.encode_utf16().collect(); - - let mut bytes = Vec::with_capacity(utf16.len() * 2 + 2); - for c in utf16 { - bytes.push((c / 256) as u8); - bytes.push((c % 256) as u8); - } - bytes.push(0x00); - bytes.push(0x00); - - bytes -} - -/// Parse PFX data into a key pair. -/// -/// PFX data is commonly encountered in `.p12` files, such as those created -/// when exporting certificates from Apple's `Keychain Access` application. -/// -/// The contents of the PFX file require a password to decrypt. However, if -/// no password was provided to create the PFX data, this password may be the -/// empty string. -pub fn parse_pfx_data( - data: &[u8], - password: &str, -) -> Result<(CapturedX509Certificate, InMemoryPrivateKey), AppleCodesignError> { - let pfx = p12::PFX::parse(data).map_err(|e| { - AppleCodesignError::PfxParseError(format!("data does not appear to be PFX: {:?}", e)) - })?; - - if !pfx.verify_mac(password) { - return Err(AppleCodesignError::PfxBadPassword); - } - - // Apple's certificate export format consists of regular data content info - // with inner ContentInfo components holding the key and certificate. - let data = match pfx.auth_safe { - p12::ContentInfo::Data(data) => data, - _ => { - return Err(AppleCodesignError::PfxParseError( - "unexpected PFX content info".to_string(), - )); - } - }; - - let content_infos = yasna::parse_der(&data, |reader| { - reader.collect_sequence_of(p12::ContentInfo::parse) - }) - .map_err(|e| { - AppleCodesignError::PfxParseError(format!("failed parsing inner ContentInfo: {:?}", e)) - })?; - - let bmp_password = bmp_string(password); - - let mut certificate = None; - let mut signing_key = None; - - for content in content_infos { - let bags_data = match content { - p12::ContentInfo::Data(inner) => inner, - p12::ContentInfo::EncryptedData(encrypted) => { - encrypted.data(&bmp_password).ok_or_else(|| { - AppleCodesignError::PfxParseError( - "failed decrypting inner EncryptedData".to_string(), - ) - })? - } - p12::ContentInfo::OtherContext(_) => { - return Err(AppleCodesignError::PfxParseError( - "unexpected OtherContent content in inner PFX data".to_string(), - )); - } - }; - - let bags = yasna::parse_ber(&bags_data, |reader| { - reader.collect_sequence_of(p12::SafeBag::parse) - }) - .map_err(|e| { - AppleCodesignError::PfxParseError(format!( - "failed parsing SafeBag within inner Data: {:?}", - e - )) - })?; - - for bag in bags { - match bag.bag { - p12::SafeBagKind::CertBag(cert_bag) => match cert_bag { - p12::CertBag::X509(cert_data) => { - certificate = Some(CapturedX509Certificate::from_der(cert_data)?); - } - p12::CertBag::SDSI(_) => { - return Err(AppleCodesignError::PfxParseError( - "unexpected SDSI certificate data".to_string(), - )); - } - }, - p12::SafeBagKind::Pkcs8ShroudedKeyBag(key_bag) => { - let decrypted = key_bag.decrypt(&bmp_password).ok_or_else(|| { - AppleCodesignError::PfxParseError( - "error decrypting PKCS8 shrouded key bag; is the password correct?" - .to_string(), - ) - })?; - - signing_key = Some(InMemoryPrivateKey::from_pkcs8_der(&decrypted)?); - } - p12::SafeBagKind::OtherBagKind(_) => { - return Err(AppleCodesignError::PfxParseError( - "unexpected bag type in inner PFX content".to_string(), - )); - } - } - } - } - - match (certificate, signing_key) { - (Some(certificate), Some(signing_key)) => Ok((certificate, signing_key)), - (None, Some(_)) => Err(AppleCodesignError::PfxParseError( - "failed to find x509 certificate in PFX data".to_string(), - )), - (_, None) => Err(AppleCodesignError::PfxParseError( - "failed to find signing key in PFX data".to_string(), - )), - } -} - -/// RSA OAEP post decrypt depadding. -/// -/// This implements the procedure described by RFC 3447 Section 7.1.2 -/// starting at Step 3 (after the ciphertext has been fed into the low-level -/// RSA decryption. -/// -/// This implementation has NOT been audited and shouldn't be used. It only -/// exists here because we need it to support RSA decryption using YubiKeys. -/// https://github.com/RustCrypto/RSA/issues/159 is fixed to hopefully get this -/// exposed as an API on the rsa crate. -#[allow(unused)] -pub(crate) fn rsa_oaep_post_decrypt_decode( - modulus_length_bytes: usize, - mut em: Vec, - digest: &mut dyn digest::DynDigest, - mgf_digest: &mut dyn digest::DynDigest, - label: Option, -) -> Result, rsa::errors::Error> { - let k = modulus_length_bytes; - let digest_len = digest.output_size(); - - // 3. EME_OAEP decoding. - - // 3a. - let label = label.unwrap_or_default(); - digest.update(label.as_bytes()); - let label_digest = digest.finalize_reset(); - - // 3b. - let (y, remaining) = em.split_at_mut(1); - let (masked_seed, masked_db) = remaining.split_at_mut(digest_len); - - if masked_seed.len() != digest_len || masked_db.len() != k - digest_len - 1 { - return Err(rsa::errors::Error::Decryption); - } - - // 3c - 3f. - mgf1_xor(masked_seed, mgf_digest, masked_db); - mgf1_xor(masked_db, mgf_digest, masked_seed); - - // 3g. - // - // We need to split into padding string (all zeroes) and message M with a - // 0x01 between them. The padding string should be all zeroes. And this should - // execute in constant time, which makes it tricky. - - let digests_equivalent = masked_db[0..digest_len].ct_eq(label_digest.as_ref()); - - let mut looking_for_index = Choice::from(1u8); - let mut index = 0u32; - let mut padding_invalid = Choice::from(0u8); - - for (i, value) in masked_db.iter().skip(digest_len).enumerate() { - let is_zero = value.ct_eq(&0u8); - let is_one = value.ct_eq(&1u8); - - index.conditional_assign(&(i as u32), looking_for_index & is_one); - looking_for_index &= !is_one; - padding_invalid |= looking_for_index & !is_zero; - } - - let y_is_zero = y[0].ct_eq(&0u8); - - let valid = y_is_zero & digests_equivalent & !padding_invalid & !looking_for_index; - - let res = CtOption::new((em, index + 2 + (digest_len * 2) as u32), valid); - - if res.is_none().into() { - return Err(rsa::errors::Error::Decryption); - } - - let (out, index) = res.unwrap(); - - Ok(out[index as usize..].to_vec()) -} - -#[cfg(test)] -mod test { - use {super::*, ring::signature::KeyPair, x509_certificate::Sign}; - - const RSA_2048_PKCS8_DER: &[u8] = include_bytes!("testdata/rsa-2048.pk8"); - const ED25519_PKCS8_DER: &[u8] = include_bytes!("testdata/ed25519.pk8"); - const SECP256_PKCS8_DER: &[u8] = include_bytes!("testdata/secp256r1.pk8"); - - #[test] - fn parse_keychain_p12_export() { - let data = include_bytes!("apple-codesign-testuser.p12"); - - let err = parse_pfx_data(data, "bad-password").unwrap_err(); - assert!(matches!(err, AppleCodesignError::PfxBadPassword)); - - parse_pfx_data(data, "password123").unwrap(); - } - - #[test] - fn rsa_key_operations() -> Result<(), AppleCodesignError> { - let ring_key = RsaKeyPair::from_pkcs8(RSA_2048_PKCS8_DER).unwrap(); - let ring_public_key_data = ring_key.public_key().as_ref(); - - let pki = PrivateKeyInfo::from_der(RSA_2048_PKCS8_DER).unwrap(); - let key = InMemoryPrivateKey::try_from(pki).unwrap(); - - assert_eq!(key.to_pkcs8_der().unwrap().as_bytes(), RSA_2048_PKCS8_DER); - - let our_key = InMemorySigningKeyPair::try_from(key)?; - let our_public_key = our_key.public_key_data(); - - assert_eq!(our_public_key.as_ref(), ring_public_key_data); - - InMemoryPrivateKey::from_pkcs8_der(RSA_2048_PKCS8_DER)?; - - Ok(()) - } - - #[test] - fn ed25519_key_operations() -> Result<(), AppleCodesignError> { - let pki = PrivateKeyInfo::from_der(ED25519_PKCS8_DER).unwrap(); - let seed = &pki.private_key[2..]; - let key = InMemoryPrivateKey::try_from(pki).unwrap(); - - assert_eq!(key.to_pkcs8_der().unwrap().as_bytes(), ED25519_PKCS8_DER); - - let our_key = InMemorySigningKeyPair::try_from(key)?; - let our_public_key = our_key.public_key_data(); - - let ring_key = Ed25519KeyPair::from_seed_unchecked(seed).unwrap(); - let ring_public_key_data = ring_key.public_key().as_ref(); - - assert_eq!(our_public_key.as_ref(), ring_public_key_data); - - InMemoryPrivateKey::from_pkcs8_der(ED25519_PKCS8_DER)?; - - Ok(()) - } - - #[test] - fn ecdsa_key_operations_secp256() -> Result<(), AppleCodesignError> { - let ring_key = EcdsaKeyPair::from_pkcs8( - &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, - SECP256_PKCS8_DER, - ) - .unwrap(); - let ring_public_key_data = ring_key.public_key().as_ref(); - - let pki = PrivateKeyInfo::from_der(SECP256_PKCS8_DER).unwrap(); - let key = InMemoryPrivateKey::try_from(pki).unwrap(); - - assert_eq!(key.to_pkcs8_der().unwrap().as_bytes(), SECP256_PKCS8_DER); - - let our_key = InMemorySigningKeyPair::try_from(key)?; - let our_public_key = our_key.public_key_data(); - - assert_eq!(our_public_key.as_ref(), ring_public_key_data); - - InMemoryPrivateKey::from_pkcs8_der(SECP256_PKCS8_DER)?; - - Ok(()) - } -} diff --git a/apple-codesign/src/dmg.rs b/apple-codesign/src/dmg.rs deleted file mode 100644 index 573d47fe5..000000000 --- a/apple-codesign/src/dmg.rs +++ /dev/null @@ -1,418 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -/*! DMG file handling. - -DMG files can have code signatures as well. However, the mechanism is a bit different -from Mach-O files. - -The last 512 bytes of a DMG are a "koly" structure, which we represent by -[KolyTrailer]. Within the [KolyTrailer] are a pair of [u64] denoting the -file offset and size of an embedded code signature. - -The embedded code signature is a signature superblob, as represented by our -[EmbeddedSignature]. - -Apple's `codesign` appears to write the Code Directory, Requirement Set, and -CMS Signature slots. However, Requirement Set is empty and the CMS blob may -have no data (just a blob header). - -Within the Code Directory, the code limit field is the offset of the start of -code signature superblob and there is exactly a single code digest. Unlike -Mach-O files which digest in 4kb chunks, the full content of the DMG up to the -superblob are digested in full. However, the page size is advertised as `1`, -which `codesign` reports as `none`. - -The Code Directory also contains a digest in the Rep Specific slot. This digest -is over the "koly" trailer, but with the u64 for the code signature size field -zeroed out. This is likely zeroed to prevent a circular dependency: you won't -know the size of the CMS payload until the signature is created so you can't -fill in a known value ahead of time. It's worth noting that for Mach-O, the -superblob is padded with zeroes so the size of the __LINKEDIT segment can be -known before the signature is made. DMG can likely get away without padding -because the "koly" trailer is at the end of the file and any junk between -the code signature and trailer will be ignored or corrupt one of the data -structures. - -The Code Directory version is 0x20100. - -DMGs are stapled by adding an additional ticket slot to the superblob. However, -this slot's digest is not recorded in the code directory, as stapling occurs -after signing and modifying the code directory would modify the code directory -and invalidate prior signatures. -*/ - -use { - crate::{ - code_directory::{CodeDirectoryBlob, CodeSignatureFlags}, - embedded_signature::{ - BlobData, CodeSigningSlot, Digest, DigestType, EmbeddedSignature, RequirementSetBlob, - }, - embedded_signature_builder::EmbeddedSignatureBuilder, - AppleCodesignError, SettingsScope, SigningSettings, - }, - log::warn, - scroll::{Pread, Pwrite, SizeWith}, - std::{ - borrow::Cow, - fs::File, - io::{Read, Seek, SeekFrom, Write}, - path::Path, - }, -}; - -const KOLY_SIZE: i64 = 512; - -/// DMG trailer describing file content. -/// -/// This is the main structure defining a DMG. -#[derive(Clone, Debug, Pread, PartialEq, Pwrite, SizeWith)] -pub struct KolyTrailer { - /// "koly" - pub signature: [u8; 4], - pub version: u32, - pub header_size: u32, - pub flags: u32, - pub running_data_fork_offset: u64, - pub data_fork_offset: u64, - pub data_fork_length: u64, - pub rsrc_fork_offset: u64, - pub rsrc_fork_length: u64, - pub segment_number: u32, - pub segment_count: u32, - pub segment_id: [u32; 4], - pub data_fork_digest_type: u32, - pub data_fork_digest_size: u32, - pub data_fork_digest: [u32; 32], - pub plist_offset: u64, - pub plist_length: u64, - pub reserved1: [u64; 8], - pub code_signature_offset: u64, - pub code_signature_size: u64, - pub reserved2: [u64; 5], - pub main_digest_type: u32, - pub main_digest_size: u32, - pub main_digest: [u32; 32], - pub image_variant: u32, - pub sector_count: u64, -} - -impl KolyTrailer { - /// Construct an instance by reading from a seekable reader. - /// - /// The trailer is the final 512 bytes of the seekable stream. - pub fn read_from(reader: &mut R) -> Result { - reader.seek(SeekFrom::End(-KOLY_SIZE))?; - - // We can't use IOread with structs larger than 256 bytes. - let mut data = vec![]; - reader.read_to_end(&mut data)?; - - let koly = data.pread_with::(0, scroll::BE)?; - - if &koly.signature != b"koly" { - return Err(AppleCodesignError::DmgBadMagic); - } - - Ok(koly) - } - - /// Obtain the offset byte after the plist data. - /// - /// This is the offset at which an embedded signature superblob would be present. - /// If no embedded signature is present, this is likely the start of [KolyTrailer]. - pub fn offset_after_plist(&self) -> u64 { - self.plist_offset + self.plist_length - } - - /// Obtain the digest of the trailer in a way compatible with code directory digesting. - /// - /// This will compute the digest of the current values but with the code signature - /// size set to 0. - pub fn digest_for_code_directory( - &self, - digest: DigestType, - ) -> Result, AppleCodesignError> { - let mut koly = self.clone(); - koly.code_signature_size = 0; - koly.code_signature_offset = self.offset_after_plist(); - - let mut buf = [0u8; KOLY_SIZE as usize]; - buf.pwrite_with(koly, 0, scroll::BE)?; - - digest.digest_data(&buf) - } -} - -/// An entity for reading DMG files. -/// -/// It only implements enough to create code signatures over the DMG. -pub struct DmgReader { - koly: KolyTrailer, - - /// Caches the embedded code signature data. - code_signature_data: Option>, -} - -impl DmgReader { - /// Construct a new instance from a reader. - pub fn new(reader: &mut R) -> Result { - let koly = KolyTrailer::read_from(reader)?; - - let code_signature_offset = koly.code_signature_offset; - let code_signature_size = koly.code_signature_size; - - let code_signature_data = if code_signature_offset != 0 && code_signature_size != 0 { - reader.seek(SeekFrom::Start(code_signature_offset))?; - let mut data = vec![]; - reader.take(code_signature_size).read_to_end(&mut data)?; - - Some(data) - } else { - None - }; - - Ok(Self { - koly, - code_signature_data, - }) - } - - /// Obtain the main data structure describing this DMG. - pub fn koly(&self) -> &KolyTrailer { - &self.koly - } - - /// Obtain the embedded code signature superblob. - pub fn embedded_signature(&self) -> Result>, AppleCodesignError> { - if let Some(data) = &self.code_signature_data { - Ok(Some(EmbeddedSignature::from_bytes(data)?)) - } else { - Ok(None) - } - } - - /// Digest an arbitrary slice of the file. - fn digest_slice_with( - &self, - digest: DigestType, - reader: &mut R, - offset: u64, - length: u64, - ) -> Result, AppleCodesignError> { - reader.seek(SeekFrom::Start(offset))?; - - let mut reader = reader.take(length); - - let mut d = digest.as_hasher()?; - - loop { - let mut buffer = [0u8; 16384]; - let count = reader.read(&mut buffer)?; - - d.update(&buffer[0..count]); - - if count == 0 { - break; - } - } - - Ok(Digest { - data: d.finish().as_ref().to_vec().into(), - }) - } - - /// Digest the content of the DMG up to the code signature or [KolyTrailer]. - /// - /// This digest is used as the code digest in the code directory. - pub fn digest_content_with( - &self, - digest: DigestType, - reader: &mut R, - ) -> Result, AppleCodesignError> { - if self.koly.code_signature_offset != 0 { - self.digest_slice_with(digest, reader, 0, self.koly.code_signature_offset) - } else { - reader.seek(SeekFrom::End(-KOLY_SIZE))?; - let size = reader.stream_position()?; - - self.digest_slice_with(digest, reader, 0, size) - } - } -} - -/// Determines whether a filesystem path is a DMG. -/// -/// Returns true if the path has a DMG trailer. -pub fn path_is_dmg(path: impl AsRef) -> Result { - let mut fh = File::open(path.as_ref())?; - - Ok(KolyTrailer::read_from(&mut fh).is_ok()) -} - -/// Entity for signing DMG files. -#[derive(Clone, Debug, Default)] -pub struct DmgSigner {} - -impl DmgSigner { - /// Sign a DMG. - /// - /// Parameters controlling the signing operation are specified by `settings`. - /// - /// `file` is a readable and writable file. The DMG signature will be written - /// into the source file. - pub fn sign_file( - &self, - settings: &SigningSettings, - fh: &mut File, - ) -> Result<(), AppleCodesignError> { - warn!("signing DMG"); - - let koly = DmgReader::new(fh)?.koly().clone(); - let signature = self.create_superblob(settings, fh)?; - - Self::write_embedded_signature(fh, koly, &signature) - } - - /// Staple a notarization ticket to a DMG. - pub fn staple_file( - &self, - fh: &mut File, - ticket_data: Vec, - ) -> Result<(), AppleCodesignError> { - warn!( - "stapling DMG with {} byte notarization ticket", - ticket_data.len() - ); - - let reader = DmgReader::new(fh)?; - let koly = reader.koly().clone(); - let signature = reader - .embedded_signature()? - .ok_or(AppleCodesignError::DmgStapleNoSignature)?; - - let mut builder = EmbeddedSignatureBuilder::new_for_stapling(signature)?; - builder.add_notarization_ticket(ticket_data)?; - - let signature = builder.create_superblob()?; - - Self::write_embedded_signature(fh, koly, &signature) - } - - fn write_embedded_signature( - fh: &mut File, - mut koly: KolyTrailer, - signature: &[u8], - ) -> Result<(), AppleCodesignError> { - warn!("writing {} byte signature", signature.len()); - fh.seek(SeekFrom::Start(koly.offset_after_plist()))?; - fh.write_all(signature)?; - - koly.code_signature_offset = koly.offset_after_plist(); - koly.code_signature_size = signature.len() as _; - - let mut trailer = [0u8; KOLY_SIZE as usize]; - trailer.pwrite_with(&koly, 0, scroll::BE)?; - - fh.write_all(&trailer)?; - - fh.set_len(koly.code_signature_offset + koly.code_signature_size + KOLY_SIZE as u64)?; - - Ok(()) - } - - /// Create the embedded signature superblob content. - pub fn create_superblob( - &self, - settings: &SigningSettings, - fh: &mut F, - ) -> Result, AppleCodesignError> { - let mut builder = EmbeddedSignatureBuilder::default(); - - for (slot, blob) in self.create_special_blobs()? { - builder.add_blob(slot, blob)?; - } - - builder.add_code_directory( - CodeSigningSlot::CodeDirectory, - self.create_code_directory(settings, fh)?, - )?; - - if let Some((signing_key, signing_cert)) = settings.signing_key() { - builder.create_cms_signature( - signing_key, - signing_cert, - settings.time_stamp_url(), - settings.certificate_chain().iter().cloned(), - )?; - } - - builder.create_superblob() - } - - /// Create the code directory data structure that is part of the embedded signature. - /// - /// This won't be the final data structure state that is serialized, as it may be - /// amended to in other functions. - pub fn create_code_directory( - &self, - settings: &SigningSettings, - fh: &mut F, - ) -> Result, AppleCodesignError> { - let reader = DmgReader::new(fh)?; - - let mut flags = settings - .code_signature_flags(SettingsScope::Main) - .unwrap_or_else(CodeSignatureFlags::empty); - - if settings.signing_key().is_some() { - flags -= CodeSignatureFlags::ADHOC; - } else { - flags |= CodeSignatureFlags::ADHOC; - } - - warn!("using code signature flags: {:?}", flags); - - let ident = Cow::Owned( - settings - .binary_identifier(SettingsScope::Main) - .ok_or(AppleCodesignError::NoIdentifier)? - .to_string(), - ); - - warn!("using identifier {}", ident); - - let code_hashes = vec![reader.digest_content_with(*settings.digest_type(), fh)?]; - - let koly_digest = reader - .koly() - .digest_for_code_directory(*settings.digest_type())?; - - let mut cd = CodeDirectoryBlob { - version: 0x20100, - flags, - code_limit: reader.koly().offset_after_plist() as u32, - digest_size: settings.digest_type().hash_len()? as u8, - digest_type: *settings.digest_type(), - page_size: 1, - ident, - code_digests: code_hashes, - ..Default::default() - }; - - cd.set_slot_digest(CodeSigningSlot::RepSpecific, koly_digest)?; - - Ok(cd) - } - - /// Create special blobs that are added to the superblob. - pub fn create_special_blobs( - &self, - ) -> Result, AppleCodesignError> { - Ok(vec![( - CodeSigningSlot::RequirementSet, - RequirementSetBlob::default().into(), - )]) - } -} diff --git a/apple-codesign/src/embedded_signature.rs b/apple-codesign/src/embedded_signature.rs deleted file mode 100644 index 0ee5ddb69..000000000 --- a/apple-codesign/src/embedded_signature.rs +++ /dev/null @@ -1,1494 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Common embedded signature data structures (superblobs, magic values, etc). -//! -//! This module defines types and data structures that are common to Apple's -//! embedded signature format. -//! -//! Within this module are constants for header magic, definitions of -//! serialized data structures like superblobs and blobs, and some common -//! enumerations. -//! -//! There is no official specification of the Mach-O structure for various -//! code signing primitives. So the definitions in here could diverge from -//! what is actually implemented. -//! -//! The best source of the specification comes from Apple's open source headers, -//! notably cs_blobs.h (e.g. -//! ). -//! (Go to and check for newer versions of xnu -//! to look for new features.) -//! -//! The high-level format of embedded signature data is roughly as follows: -//! -//! * A `SuperBlob` header describes the total length of data and the number of -//! *blob* sections that follow. -//! * An array of `BlobIndex` describing the type and offset of all *blob* sections -//! that follow. The *type* here is a *slot* and describes what type of data the -//! *blob* contains (code directory, entitlements, embedded signature, etc). -//! * N *blob* sections of varying formats and lengths. -//! -//! We only support the [CodeSigningMagic::EmbeddedSignature] magic in the `SuperBlob`, -//! as this is what is used in the wild. (It is even unclear if other magic values -//! can occur in `SuperBlob` headers.) -//! -//! The `EmbeddedSignature` type represents a lightly parsed `SuperBlob`. It -//! provides access to `BlobEntry` which describe the *blob* sections within the -//! super blob. A `BlobEntry` can be parsed into the more concrete `ParsedBlob`, -//! which allows some access to data within each specific blob type. - -use { - crate::{ - code_directory::CodeDirectoryBlob, code_requirement::CodeRequirements, - code_requirement::RequirementType, AppleCodesignError, - }, - apple_xar::table_of_contents::ChecksumType as XarChecksumType, - cryptographic_message_syntax::SignedData, - scroll::{IOwrite, Pread}, - std::{ - borrow::Cow, - cmp::Ordering, - collections::HashMap, - fmt::{Display, Formatter}, - io::Write, - }, - x509_certificate::DigestAlgorithm, -}; - -/// Defines header magic for various payloads. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum CodeSigningMagic { - /// Code requirement blob. - Requirement, - /// Code requirements blob. - RequirementSet, - /// CodeDirectory blob. - CodeDirectory, - /// Embedded signature. - /// - /// This is often the magic of the SuperBlob. - EmbeddedSignature, - /// Old embedded signature. - EmbeddedSignatureOld, - /// Entitlements blob. - Entitlements, - /// DER encoded entitlements blob. - EntitlementsDer, - /// Multi-arch collection of embedded signatures. - DetachedSignature, - /// Generic blob wrapper. - /// - /// The CMS signature is stored in this type. - BlobWrapper, - /// Unknown magic. - Unknown(u32), -} - -impl From for CodeSigningMagic { - fn from(v: u32) -> Self { - match v { - 0xfade0c00 => Self::Requirement, - 0xfade0c01 => Self::RequirementSet, - 0xfade0c02 => Self::CodeDirectory, - 0xfade0cc0 => Self::EmbeddedSignature, - 0xfade0b02 => Self::EmbeddedSignatureOld, - 0xfade7171 => Self::Entitlements, - 0xfade7172 => Self::EntitlementsDer, - 0xfade0cc1 => Self::DetachedSignature, - 0xfade0b01 => Self::BlobWrapper, - _ => Self::Unknown(v), - } - } -} - -impl From for u32 { - fn from(magic: CodeSigningMagic) -> u32 { - match magic { - CodeSigningMagic::Requirement => 0xfade0c00, - CodeSigningMagic::RequirementSet => 0xfade0c01, - CodeSigningMagic::CodeDirectory => 0xfade0c02, - CodeSigningMagic::EmbeddedSignature => 0xfade0cc0, - CodeSigningMagic::EmbeddedSignatureOld => 0xfade0b02, - CodeSigningMagic::Entitlements => 0xfade7171, - CodeSigningMagic::EntitlementsDer => 0xfade7172, - CodeSigningMagic::DetachedSignature => 0xfade0cc1, - CodeSigningMagic::BlobWrapper => 0xfade0b01, - CodeSigningMagic::Unknown(v) => v, - } - } -} - -/// A well-known slot within code signing data. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub enum CodeSigningSlot { - CodeDirectory, - /// Info.plist. - Info, - /// Designated requirements. - RequirementSet, - /// Digest of `CodeRequirements` file (used in bundles). - ResourceDir, - /// Application specific slot. - Application, - /// Entitlements XML plist. - Entitlements, - /// Reserved for disk images. - RepSpecific, - /// Entitlements DER encoded plist. - EntitlementsDer, - // Everything from here is a slot not encoded in the code directory hashes list. - // REMEMBER TO UPDATE is_code_directory_specials_expressible() if adding a new slot - // here! - /// Alternative code directory slot #0. - /// - /// Used for expressing a code directory using an alternate digest type. - AlternateCodeDirectory0, - AlternateCodeDirectory1, - AlternateCodeDirectory2, - AlternateCodeDirectory3, - AlternateCodeDirectory4, - /// CMS signature. - Signature, - Identification, - /// Notarization ticket. - Ticket, - Unknown(u32), -} - -impl std::fmt::Debug for CodeSigningSlot { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::CodeDirectory => { - f.write_fmt(format_args!("CodeDirectory ({})", u32::from(*self))) - } - Self::Info => f.write_fmt(format_args!("Info ({})", u32::from(*self))), - Self::RequirementSet => { - f.write_fmt(format_args!("RequirementSet ({})", u32::from(*self))) - } - Self::ResourceDir => f.write_fmt(format_args!("Resources ({})", u32::from(*self))), - Self::Application => f.write_fmt(format_args!("Application ({})", u32::from(*self))), - Self::Entitlements => f.write_fmt(format_args!("Entitlements ({})", u32::from(*self))), - Self::RepSpecific => f.write_fmt(format_args!("Rep Specific ({})", u32::from(*self))), - Self::EntitlementsDer => { - f.write_fmt(format_args!("DER Entitlements ({})", u32::from(*self))) - } - Self::AlternateCodeDirectory0 => f.write_fmt(format_args!( - "CodeDirectory Alternate #0 ({})", - u32::from(*self) - )), - Self::AlternateCodeDirectory1 => f.write_fmt(format_args!( - "CodeDirectory Alternate #1 ({})", - u32::from(*self) - )), - Self::AlternateCodeDirectory2 => f.write_fmt(format_args!( - "CodeDirectory Alternate #2 ({})", - u32::from(*self) - )), - Self::AlternateCodeDirectory3 => f.write_fmt(format_args!( - "CodeDirectory Alternate #3 ({})", - u32::from(*self) - )), - Self::AlternateCodeDirectory4 => f.write_fmt(format_args!( - "CodeDirectory Alternate #4 ({})", - u32::from(*self) - )), - Self::Signature => f.write_fmt(format_args!("CMS Signature ({})", u32::from(*self))), - Self::Identification => { - f.write_fmt(format_args!("Identification ({})", u32::from(*self))) - } - Self::Ticket => f.write_fmt(format_args!("Ticket ({})", u32::from(*self))), - Self::Unknown(value) => f.write_fmt(format_args!("Unknown ({})", value)), - } - } -} - -impl From for CodeSigningSlot { - fn from(v: u32) -> Self { - match v { - 0 => Self::CodeDirectory, - 1 => Self::Info, - 2 => Self::RequirementSet, - 3 => Self::ResourceDir, - 4 => Self::Application, - 5 => Self::Entitlements, - 6 => Self::RepSpecific, - 7 => Self::EntitlementsDer, - 0x1000 => Self::AlternateCodeDirectory0, - 0x1001 => Self::AlternateCodeDirectory1, - 0x1002 => Self::AlternateCodeDirectory2, - 0x1003 => Self::AlternateCodeDirectory3, - 0x1004 => Self::AlternateCodeDirectory4, - 0x10000 => Self::Signature, - 0x10001 => Self::Identification, - 0x10002 => Self::Ticket, - _ => Self::Unknown(v), - } - } -} - -impl From for u32 { - fn from(v: CodeSigningSlot) -> Self { - match v { - CodeSigningSlot::CodeDirectory => 0, - CodeSigningSlot::Info => 1, - CodeSigningSlot::RequirementSet => 2, - CodeSigningSlot::ResourceDir => 3, - CodeSigningSlot::Application => 4, - CodeSigningSlot::Entitlements => 5, - CodeSigningSlot::RepSpecific => 6, - CodeSigningSlot::EntitlementsDer => 7, - CodeSigningSlot::AlternateCodeDirectory0 => 0x1000, - CodeSigningSlot::AlternateCodeDirectory1 => 0x1001, - CodeSigningSlot::AlternateCodeDirectory2 => 0x1002, - CodeSigningSlot::AlternateCodeDirectory3 => 0x1003, - CodeSigningSlot::AlternateCodeDirectory4 => 0x1004, - CodeSigningSlot::Signature => 0x10000, - CodeSigningSlot::Identification => 0x10001, - CodeSigningSlot::Ticket => 0x10002, - CodeSigningSlot::Unknown(v) => v, - } - } -} - -impl PartialOrd for CodeSigningSlot { - fn partial_cmp(&self, other: &Self) -> Option { - u32::from(*self).partial_cmp(&u32::from(*other)) - } -} - -impl Ord for CodeSigningSlot { - fn cmp(&self, other: &Self) -> Ordering { - u32::from(*self).cmp(&u32::from(*other)) - } -} - -impl CodeSigningSlot { - /// Whether this slot has external data (as opposed to provided via a blob). - pub fn has_external_content(&self) -> bool { - matches!(self, Self::Info | Self::ResourceDir) - } - - /// Whether this slot is for holding an alternative code directory. - pub fn is_alternative_code_directory(&self) -> bool { - matches!( - self, - CodeSigningSlot::AlternateCodeDirectory0 - | CodeSigningSlot::AlternateCodeDirectory1 - | CodeSigningSlot::AlternateCodeDirectory2 - | CodeSigningSlot::AlternateCodeDirectory3 - | CodeSigningSlot::AlternateCodeDirectory4 - ) - } - - /// Whether this slot's digest is expressed in code directories list of special slot digests. - pub fn is_code_directory_specials_expressible(&self) -> bool { - *self >= Self::Info && *self <= Self::EntitlementsDer - } -} - -#[repr(C)] -#[derive(Clone, Pread)] -struct BlobIndex { - /// Corresponds to a [CodeSigningSlot] variant. - typ: u32, - offset: u32, -} - -impl std::fmt::Debug for BlobIndex { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.debug_struct("BlobIndex") - .field("type", &CodeSigningSlot::from(self.typ)) - .field("offset", &self.offset) - .finish() - } -} - -/// Represents a digest type encountered in code signature data structures. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum DigestType { - None, - Sha1, - Sha256, - Sha256Truncated, - Sha384, - Sha512, - Unknown(u8), -} - -impl Default for DigestType { - fn default() -> Self { - Self::Sha256 - } -} - -impl From for DigestType { - fn from(v: u8) -> Self { - match v { - 0 => Self::None, - 1 => Self::Sha1, - 2 => Self::Sha256, - 3 => Self::Sha256Truncated, - 4 => Self::Sha384, - 5 => Self::Sha512, - _ => Self::Unknown(v), - } - } -} - -impl From for u8 { - fn from(v: DigestType) -> u8 { - match v { - DigestType::None => 0, - DigestType::Sha1 => 1, - DigestType::Sha256 => 2, - DigestType::Sha256Truncated => 3, - DigestType::Sha384 => 4, - DigestType::Sha512 => 5, - DigestType::Unknown(v) => v, - } - } -} - -impl TryFrom for DigestAlgorithm { - type Error = AppleCodesignError; - - fn try_from(value: DigestType) -> Result { - match value { - DigestType::Sha1 => Ok(DigestAlgorithm::Sha1), - DigestType::Sha256 => Ok(DigestAlgorithm::Sha256), - DigestType::Sha256Truncated => Ok(DigestAlgorithm::Sha256), - DigestType::Sha384 => Ok(DigestAlgorithm::Sha384), - DigestType::Sha512 => Ok(DigestAlgorithm::Sha512), - DigestType::Unknown(_) => Err(AppleCodesignError::DigestUnknownAlgorithm), - DigestType::None => Err(AppleCodesignError::DigestUnsupportedAlgorithm), - } - } -} - -impl PartialOrd for DigestType { - fn partial_cmp(&self, other: &Self) -> Option { - u8::from(*self).partial_cmp(&u8::from(*other)) - } -} - -impl Ord for DigestType { - fn cmp(&self, other: &Self) -> Ordering { - u8::from(*self).cmp(&u8::from(*other)) - } -} - -impl Display for DigestType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - DigestType::None => f.write_str("none"), - DigestType::Sha1 => f.write_str("sha1"), - DigestType::Sha256 => f.write_str("sha256"), - DigestType::Sha256Truncated => f.write_str("sha256-truncated"), - DigestType::Sha384 => f.write_str("sha384"), - DigestType::Sha512 => f.write_str("sha512"), - DigestType::Unknown(v) => f.write_fmt(format_args!("unknown: {}", v)), - } - } -} - -impl TryFrom<&str> for DigestType { - type Error = AppleCodesignError; - - fn try_from(s: &str) -> Result { - match s { - "none" => Ok(Self::None), - "sha1" => Ok(Self::Sha1), - "sha256" => Ok(Self::Sha256), - "sha256-truncated" => Ok(Self::Sha256Truncated), - "sha384" => Ok(Self::Sha384), - "sha512" => Ok(Self::Sha512), - _ => Err(AppleCodesignError::DigestUnknownAlgorithm), - } - } -} - -impl TryFrom for DigestType { - type Error = AppleCodesignError; - - fn try_from(c: XarChecksumType) -> Result { - match c { - XarChecksumType::None => Ok(Self::None), - XarChecksumType::Sha1 => Ok(Self::Sha1), - XarChecksumType::Sha256 => Ok(Self::Sha256), - XarChecksumType::Sha512 => Ok(Self::Sha512), - XarChecksumType::Md5 => Err(AppleCodesignError::DigestUnsupportedAlgorithm), - } - } -} - -impl DigestType { - /// Obtain the size of hashes for this hash type. - pub fn hash_len(&self) -> Result { - Ok(self.digest_data(&[])?.len()) - } - - /// Obtain a hasher for this digest type. - pub fn as_hasher(&self) -> Result { - match self { - Self::None => Err(AppleCodesignError::DigestUnknownAlgorithm), - Self::Sha1 => Ok(ring::digest::Context::new( - &ring::digest::SHA1_FOR_LEGACY_USE_ONLY, - )), - Self::Sha256 | Self::Sha256Truncated => { - Ok(ring::digest::Context::new(&ring::digest::SHA256)) - } - Self::Sha384 => Ok(ring::digest::Context::new(&ring::digest::SHA384)), - Self::Sha512 => Ok(ring::digest::Context::new(&ring::digest::SHA512)), - Self::Unknown(_) => Err(AppleCodesignError::DigestUnknownAlgorithm), - } - } - - /// Digest data given the configured hasher. - pub fn digest_data(&self, data: &[u8]) -> Result, AppleCodesignError> { - let mut hasher = self.as_hasher()?; - - hasher.update(data); - let mut hash = hasher.finish().as_ref().to_vec(); - - if matches!(self, Self::Sha256Truncated) { - hash.truncate(20); - } - - Ok(hash) - } -} - -pub struct Digest<'a> { - pub data: Cow<'a, [u8]>, -} - -impl<'a> Digest<'a> { - /// Whether this is the null hash (all 0s). - pub fn is_null(&self) -> bool { - self.data.iter().all(|b| *b == 0) - } - - pub fn to_vec(&self) -> Vec { - self.data.to_vec() - } - - pub fn to_owned(&self) -> Digest<'static> { - Digest { - data: Cow::Owned(self.data.clone().into_owned()), - } - } - - pub fn as_hex(&self) -> String { - hex::encode(&self.data) - } -} - -impl<'a> std::fmt::Debug for Digest<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&hex::encode(&self.data)) - } -} - -impl<'a> From> for Digest<'a> { - fn from(v: Vec) -> Self { - Self { data: v.into() } - } -} - -/// Read the header from a Blob. -/// -/// Blobs begin with a u32 magic and u32 length, inclusive. -fn read_blob_header(data: &[u8]) -> Result<(u32, usize, &[u8]), scroll::Error> { - let magic = data.pread_with(0, scroll::BE)?; - let length = data.pread_with::(4, scroll::BE)?; - - Ok((magic, length as usize, &data[8..])) -} - -pub(crate) fn read_and_validate_blob_header<'a>( - data: &'a [u8], - expected_magic: u32, - what: &'static str, -) -> Result<&'a [u8], AppleCodesignError> { - let (magic, _, data) = read_blob_header(data)?; - - if magic != expected_magic { - Err(AppleCodesignError::BadMagic(what)) - } else { - Ok(data) - } -} - -/// Create the binary content for a SuperBlob. -pub fn create_superblob<'a>( - magic: CodeSigningMagic, - blobs: impl Iterator)>, -) -> Result, AppleCodesignError> { - // Makes offset calculation easier. - let blobs = blobs.collect::>(); - - let mut cursor = std::io::Cursor::new(Vec::::new()); - - let mut blob_data = Vec::new(); - // magic + total length + blob count. - let mut total_length: u32 = 4 + 4 + 4; - // 8 bytes for each blob index. - total_length += 8 * blobs.len() as u32; - - let mut indices = Vec::with_capacity(blobs.len()); - - for (slot, blob) in blobs { - blob_data.push(blob); - - indices.push(BlobIndex { - typ: u32::from(*slot), - offset: total_length, - }); - - total_length += blob.len() as u32; - } - - cursor.iowrite_with(u32::from(magic), scroll::BE)?; - cursor.iowrite_with(total_length, scroll::BE)?; - cursor.iowrite_with(indices.len() as u32, scroll::BE)?; - for index in indices { - cursor.iowrite_with(index.typ, scroll::BE)?; - cursor.iowrite_with(index.offset, scroll::BE)?; - } - for data in blob_data { - cursor.write_all(data)?; - } - - Ok(cursor.into_inner()) -} - -/// Represents a single blob as defined by a SuperBlob index entry. -/// -/// Instances have copies of their own index info, including the relative -/// order, slot type, and start offset within the `SuperBlob`. -/// -/// The blob data is unparsed in this type. The blob payloads can be -/// turned into [ParsedBlob] via `.try_into()`. -#[derive(Clone)] -pub struct BlobEntry<'a> { - /// Our blob index within the `SuperBlob`. - pub index: usize, - - /// The slot type. - pub slot: CodeSigningSlot, - - /// Our start offset within the `SuperBlob`. - /// - /// First byte is start of our magic. - pub offset: usize, - - /// The magic value appearing at the beginning of the blob. - pub magic: CodeSigningMagic, - - /// The length of the blob payload. - pub length: usize, - - /// The raw data in this blob, including magic and length. - pub data: &'a [u8], -} - -impl<'a> std::fmt::Debug for BlobEntry<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.debug_struct("BlobEntry") - .field("index", &self.index) - .field("slot", &self.slot) - .field("offset", &self.offset) - .field("length", &self.length) - .field("magic", &self.magic) - // .field("data", &self.data) - .finish() - } -} - -impl<'a> BlobEntry<'a> { - /// Attempt to convert to a [ParsedBlob]. - pub fn into_parsed_blob(self) -> Result, AppleCodesignError> { - self.try_into() - } - - /// Obtain the payload of this blob. - /// - /// This is the data in the blob without the blob header. - pub fn payload(&self) -> Result<&'a [u8], AppleCodesignError> { - Ok(read_blob_header(self.data)?.2) - } - - /// Compute the content digest of this blob using the specified hash type. - pub fn digest_with(&self, hash: DigestType) -> Result, AppleCodesignError> { - hash.digest_data(self.data) - } -} - -/// Provides common features for a parsed blob type. -pub trait Blob<'a> -where - Self: Sized, -{ - /// The header magic that identifies this format. - fn magic() -> u32; - - /// Attempt to construct an instance by parsing a bytes slice. - /// - /// The slice begins with the 8 byte blob header denoting the magic - /// and length. - fn from_blob_bytes(data: &'a [u8]) -> Result; - - /// Serialize the payload of this blob to bytes. - /// - /// Does not include the magic or length header fields common to blobs. - fn serialize_payload(&self) -> Result, AppleCodesignError>; - - /// Serialize this blob to bytes. - /// - /// This is [Blob::serialize_payload] with the blob magic and length - /// prepended. - fn to_blob_bytes(&self) -> Result, AppleCodesignError> { - let mut res = Vec::new(); - res.iowrite_with(Self::magic(), scroll::BE)?; - - let payload = self.serialize_payload()?; - // Length includes our own header. - res.iowrite_with(payload.len() as u32 + 8, scroll::BE)?; - - res.extend(payload); - - Ok(res) - } - - /// Obtain the digest of the blob using the specified hasher. - /// - /// Default implementation calls [Blob::to_blob_bytes] and digests that, which - /// should always be correct. - fn digest_with(&self, hash_type: DigestType) -> Result, AppleCodesignError> { - hash_type.digest_data(&self.to_blob_bytes()?) - } -} - -/// Represents a Requirement blob. -/// -/// `csreq -b` will emit instances of this blob, header magic and all. So data generated -/// by `csreq -b` can be fed into [RequirementBlob.from_blob_bytes] to obtain an instance. -pub struct RequirementBlob<'a> { - pub data: Cow<'a, [u8]>, -} - -impl<'a> Blob<'a> for RequirementBlob<'a> { - fn magic() -> u32 { - u32::from(CodeSigningMagic::Requirement) - } - - fn from_blob_bytes(data: &'a [u8]) -> Result { - let data = read_and_validate_blob_header(data, Self::magic(), "requirement blob")?; - - Ok(Self { data: data.into() }) - } - - fn serialize_payload(&self) -> Result, AppleCodesignError> { - Ok(self.data.to_vec()) - } -} - -impl<'a> std::fmt::Debug for RequirementBlob<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("RequirementBlob({})", hex::encode(&self.data))) - } -} - -impl<'a> RequirementBlob<'a> { - pub fn to_owned(&self) -> RequirementBlob<'static> { - RequirementBlob { - data: Cow::Owned(self.data.clone().into_owned()), - } - } - - /// Parse the binary data in this blob into Code Requirement expressions. - pub fn parse_expressions(&self) -> Result { - Ok(CodeRequirements::parse_binary(&self.data)?.0) - } -} - -/// Represents a Requirement set blob. -/// -/// A Requirement set blob contains nested Requirement blobs. -#[derive(Debug, Default)] -pub struct RequirementSetBlob<'a> { - pub requirements: HashMap>, -} - -impl<'a> Blob<'a> for RequirementSetBlob<'a> { - fn magic() -> u32 { - u32::from(CodeSigningMagic::RequirementSet) - } - - fn from_blob_bytes(data: &'a [u8]) -> Result { - read_and_validate_blob_header(data, Self::magic(), "requirement set blob")?; - - // There are other blobs nested within. A u32 denotes how many there are. - // Then there is an array of N (u32, u32) denoting the type and - // offset of each. - let offset = &mut 8; - let count = data.gread_with::(offset, scroll::BE)?; - - let mut indices = Vec::with_capacity(count as usize); - for _ in 0..count { - indices.push(( - data.gread_with::(offset, scroll::BE)?, - data.gread_with::(offset, scroll::BE)?, - )); - } - - let mut requirements = HashMap::with_capacity(indices.len()); - - for (i, (flavor, offset)) in indices.iter().enumerate() { - let typ = RequirementType::from(*flavor); - - let end_offset = if i == indices.len() - 1 { - data.len() - } else { - indices[i + 1].1 as usize - }; - - let requirement_data = &data[*offset as usize..end_offset]; - - requirements.insert(typ, RequirementBlob::from_blob_bytes(requirement_data)?); - } - - Ok(Self { requirements }) - } - - fn serialize_payload(&self) -> Result, AppleCodesignError> { - let mut res = Vec::new(); - - // The index contains blob relative offsets. To know what the start offset will - // be, we calculate the total index size. - let data_start_offset = 8 + 4 + (8 * self.requirements.len() as u32); - let mut written_requirements_data = 0; - - res.iowrite_with(self.requirements.len() as u32, scroll::BE)?; - - // Write an index of all nested requirement blobs. - for (typ, requirement) in &self.requirements { - res.iowrite_with(u32::from(*typ), scroll::BE)?; - res.iowrite_with(data_start_offset + written_requirements_data, scroll::BE)?; - written_requirements_data += requirement.to_blob_bytes()?.len() as u32; - } - - // Now write every requirement's raw data. - for requirement in self.requirements.values() { - res.write_all(&requirement.to_blob_bytes()?)?; - } - - Ok(res) - } -} - -impl<'a> RequirementSetBlob<'a> { - pub fn to_owned(&self) -> RequirementSetBlob<'static> { - RequirementSetBlob { - requirements: self - .requirements - .iter() - .map(|(flavor, blob)| (*flavor, blob.to_owned())) - .collect::>(), - } - } - - /// Set the requirements for a given [RequirementType]. - pub fn set_requirements(&mut self, slot: RequirementType, blob: RequirementBlob<'a>) { - self.requirements.insert(slot, blob); - } -} - -/// Represents an embedded signature. -#[derive(Debug)] -pub struct EmbeddedSignatureBlob<'a> { - data: &'a [u8], -} - -impl<'a> Blob<'a> for EmbeddedSignatureBlob<'a> { - fn magic() -> u32 { - u32::from(CodeSigningMagic::EmbeddedSignature) - } - - fn from_blob_bytes(data: &'a [u8]) -> Result { - Ok(Self { - data: read_and_validate_blob_header(data, Self::magic(), "embedded signature blob")?, - }) - } - - fn serialize_payload(&self) -> Result, AppleCodesignError> { - Ok(self.data.to_vec()) - } -} - -/// An old embedded signature. -#[derive(Debug)] -pub struct EmbeddedSignatureOldBlob<'a> { - data: &'a [u8], -} - -impl<'a> Blob<'a> for EmbeddedSignatureOldBlob<'a> { - fn magic() -> u32 { - u32::from(CodeSigningMagic::EmbeddedSignatureOld) - } - - fn from_blob_bytes(data: &'a [u8]) -> Result { - Ok(Self { - data: read_and_validate_blob_header( - data, - Self::magic(), - "old embedded signature blob", - )?, - }) - } - - fn serialize_payload(&self) -> Result, AppleCodesignError> { - Ok(self.data.to_vec()) - } -} - -/// Represents an Entitlements blob. -/// -/// An entitlements blob contains an XML plist with a dict. Keys are -/// strings of the entitlements being requested and values appear to be -/// simple bools. -#[derive(Debug)] -pub struct EntitlementsBlob<'a> { - plist: Cow<'a, str>, -} - -impl<'a> Blob<'a> for EntitlementsBlob<'a> { - fn magic() -> u32 { - u32::from(CodeSigningMagic::Entitlements) - } - - fn from_blob_bytes(data: &'a [u8]) -> Result { - let data = read_and_validate_blob_header(data, Self::magic(), "entitlements blob")?; - let s = std::str::from_utf8(data).map_err(AppleCodesignError::EntitlementsBadUtf8)?; - - Ok(Self { plist: s.into() }) - } - - fn serialize_payload(&self) -> Result, AppleCodesignError> { - Ok(self.plist.as_bytes().to_vec()) - } -} - -impl<'a> EntitlementsBlob<'a> { - /// Construct an instance using any string as the payload. - pub fn from_string(s: &(impl ToString + ?Sized)) -> Self { - Self { - plist: s.to_string().into(), - } - } - - /// Obtain the plist representation as a string. - pub fn as_str(&self) -> &str { - &self.plist - } -} - -impl<'a> std::fmt::Display for EntitlementsBlob<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.plist) - } -} - -#[derive(Debug)] -pub struct EntitlementsDerBlob<'a> { - der: Cow<'a, [u8]>, -} - -impl<'a> Blob<'a> for EntitlementsDerBlob<'a> { - fn magic() -> u32 { - u32::from(CodeSigningMagic::EntitlementsDer) - } - - fn from_blob_bytes(data: &'a [u8]) -> Result { - let der = read_and_validate_blob_header(data, Self::magic(), "DER entitlements blob")?; - - Ok(Self { der: der.into() }) - } - - fn serialize_payload(&self) -> Result, AppleCodesignError> { - Ok(self.der.to_vec()) - } -} - -impl<'a> EntitlementsDerBlob<'a> { - /// Construct an instance from a [plist::Value]. - /// - /// Not all plists can be encoded to this blob as not all plist value types can - /// be encoded to DER. If a plist with an illegal value is passed in, this - /// function will error, as DER encoding is performed immediately. - /// - /// The outermost plist value should be a dictionary. - pub fn from_plist(v: &plist::Value) -> Result { - let der = crate::entitlements::der_encode_entitlements_plist(v)?; - - Ok(Self { der: der.into() }) - } -} - -/// A detached signature. -#[derive(Debug)] -pub struct DetachedSignatureBlob<'a> { - data: &'a [u8], -} - -impl<'a> Blob<'a> for DetachedSignatureBlob<'a> { - fn magic() -> u32 { - u32::from(CodeSigningMagic::DetachedSignature) - } - - fn from_blob_bytes(data: &'a [u8]) -> Result { - Ok(Self { - data: read_and_validate_blob_header(data, Self::magic(), "detached signature blob")?, - }) - } - - fn serialize_payload(&self) -> Result, AppleCodesignError> { - Ok(self.data.to_vec()) - } -} - -/// Represents a generic blob wrapper. -pub struct BlobWrapperBlob<'a> { - data: Cow<'a, [u8]>, -} - -impl<'a> Blob<'a> for BlobWrapperBlob<'a> { - fn magic() -> u32 { - u32::from(CodeSigningMagic::BlobWrapper) - } - - fn from_blob_bytes(data: &'a [u8]) -> Result { - Ok(Self { - data: read_and_validate_blob_header(data, Self::magic(), "blob wrapper blob")?.into(), - }) - } - - fn serialize_payload(&self) -> Result, AppleCodesignError> { - Ok(self.data.to_vec()) - } -} - -impl<'a> std::fmt::Debug for BlobWrapperBlob<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", hex::encode(&self.data))) - } -} - -impl<'a> BlobWrapperBlob<'a> { - /// Construct an instance where the payload (post blob header) is given data. - pub fn from_data_borrowed(data: &'a [u8]) -> BlobWrapperBlob<'a> { - Self { data: data.into() } - } -} - -impl<'a> BlobWrapperBlob<'static> { - /// Construct an instance with payload data. - pub fn from_data_owned(data: Vec) -> BlobWrapperBlob<'static> { - Self { data: data.into() } - } -} - -/// Represents an unknown blob type. -pub struct OtherBlob<'a> { - pub magic: u32, - pub data: &'a [u8], -} - -impl<'a> Blob<'a> for OtherBlob<'a> { - fn magic() -> u32 { - // Use a placeholder magic value because there is no self bind here. - u32::MAX - } - - fn from_blob_bytes(data: &'a [u8]) -> Result { - let (magic, _, data) = read_blob_header(data)?; - - Ok(Self { magic, data }) - } - - fn serialize_payload(&self) -> Result, AppleCodesignError> { - Ok(self.data.to_vec()) - } - - // We need to implement this for custom magic serialization. - fn to_blob_bytes(&self) -> Result, AppleCodesignError> { - let mut res = Vec::with_capacity(self.data.len() + 8); - res.iowrite_with(self.magic, scroll::BE)?; - res.iowrite_with(self.data.len() as u32 + 8, scroll::BE)?; - res.write_all(self.data)?; - - Ok(res) - } -} - -impl<'a> std::fmt::Debug for OtherBlob<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", hex::encode(self.data))) - } -} - -/// Represents a single, parsed Blob entry/slot. -/// -/// Each variant corresponds to a [CodeSigningMagic] blob type. -#[derive(Debug)] -pub enum BlobData<'a> { - Requirement(Box>), - RequirementSet(Box>), - CodeDirectory(Box>), - EmbeddedSignature(Box>), - EmbeddedSignatureOld(Box>), - Entitlements(Box>), - EntitlementsDer(Box>), - DetachedSignature(Box>), - BlobWrapper(Box>), - Other(Box>), -} - -impl<'a> Blob<'a> for BlobData<'a> { - fn magic() -> u32 { - u32::MAX - } - - /// Parse blob data by reading its magic and feeding into magic-specific parser. - fn from_blob_bytes(data: &'a [u8]) -> Result { - let (magic, length, _) = read_blob_header(data)?; - - // This should be a no-op. But it could (correctly) cause a panic if the - // advertised length is incorrect and we would incur a buffer overrun. - let data = &data[0..length]; - - let magic = CodeSigningMagic::from(magic); - - Ok(match magic { - CodeSigningMagic::Requirement => { - Self::Requirement(Box::new(RequirementBlob::from_blob_bytes(data)?)) - } - CodeSigningMagic::RequirementSet => { - Self::RequirementSet(Box::new(RequirementSetBlob::from_blob_bytes(data)?)) - } - CodeSigningMagic::CodeDirectory => { - Self::CodeDirectory(Box::new(CodeDirectoryBlob::from_blob_bytes(data)?)) - } - CodeSigningMagic::EmbeddedSignature => { - Self::EmbeddedSignature(Box::new(EmbeddedSignatureBlob::from_blob_bytes(data)?)) - } - CodeSigningMagic::EmbeddedSignatureOld => Self::EmbeddedSignatureOld(Box::new( - EmbeddedSignatureOldBlob::from_blob_bytes(data)?, - )), - CodeSigningMagic::Entitlements => { - Self::Entitlements(Box::new(EntitlementsBlob::from_blob_bytes(data)?)) - } - CodeSigningMagic::EntitlementsDer => { - Self::EntitlementsDer(Box::new(EntitlementsDerBlob::from_blob_bytes(data)?)) - } - CodeSigningMagic::DetachedSignature => { - Self::DetachedSignature(Box::new(DetachedSignatureBlob::from_blob_bytes(data)?)) - } - CodeSigningMagic::BlobWrapper => { - Self::BlobWrapper(Box::new(BlobWrapperBlob::from_blob_bytes(data)?)) - } - _ => Self::Other(Box::new(OtherBlob::from_blob_bytes(data)?)), - }) - } - - fn serialize_payload(&self) -> Result, AppleCodesignError> { - match self { - Self::Requirement(b) => b.serialize_payload(), - Self::RequirementSet(b) => b.serialize_payload(), - Self::CodeDirectory(b) => b.serialize_payload(), - Self::EmbeddedSignature(b) => b.serialize_payload(), - Self::EmbeddedSignatureOld(b) => b.serialize_payload(), - Self::Entitlements(b) => b.serialize_payload(), - Self::EntitlementsDer(b) => b.serialize_payload(), - Self::DetachedSignature(b) => b.serialize_payload(), - Self::BlobWrapper(b) => b.serialize_payload(), - Self::Other(b) => b.serialize_payload(), - } - } - - fn to_blob_bytes(&self) -> Result, AppleCodesignError> { - match self { - Self::Requirement(b) => b.to_blob_bytes(), - Self::RequirementSet(b) => b.to_blob_bytes(), - Self::CodeDirectory(b) => b.to_blob_bytes(), - Self::EmbeddedSignature(b) => b.to_blob_bytes(), - Self::EmbeddedSignatureOld(b) => b.to_blob_bytes(), - Self::Entitlements(b) => b.to_blob_bytes(), - Self::EntitlementsDer(b) => b.to_blob_bytes(), - Self::DetachedSignature(b) => b.to_blob_bytes(), - Self::BlobWrapper(b) => b.to_blob_bytes(), - Self::Other(b) => b.to_blob_bytes(), - } - } -} - -impl<'a> From> for BlobData<'a> { - fn from(b: RequirementBlob<'a>) -> Self { - Self::Requirement(Box::new(b)) - } -} - -impl<'a> From> for BlobData<'a> { - fn from(b: RequirementSetBlob<'a>) -> Self { - Self::RequirementSet(Box::new(b)) - } -} - -impl<'a> From> for BlobData<'a> { - fn from(b: CodeDirectoryBlob<'a>) -> Self { - Self::CodeDirectory(Box::new(b)) - } -} - -impl<'a> From> for BlobData<'a> { - fn from(b: EmbeddedSignatureBlob<'a>) -> Self { - Self::EmbeddedSignature(Box::new(b)) - } -} - -impl<'a> From> for BlobData<'a> { - fn from(b: EmbeddedSignatureOldBlob<'a>) -> Self { - Self::EmbeddedSignatureOld(Box::new(b)) - } -} - -impl<'a> From> for BlobData<'a> { - fn from(b: EntitlementsBlob<'a>) -> Self { - Self::Entitlements(Box::new(b)) - } -} - -impl<'a> From> for BlobData<'a> { - fn from(b: EntitlementsDerBlob<'a>) -> Self { - Self::EntitlementsDer(Box::new(b)) - } -} - -impl<'a> From> for BlobData<'a> { - fn from(b: DetachedSignatureBlob<'a>) -> Self { - Self::DetachedSignature(Box::new(b)) - } -} - -impl<'a> From> for BlobData<'a> { - fn from(b: BlobWrapperBlob<'a>) -> Self { - Self::BlobWrapper(Box::new(b)) - } -} - -impl<'a> From> for BlobData<'a> { - fn from(b: OtherBlob<'a>) -> Self { - Self::Other(Box::new(b)) - } -} - -/// Represents the parsed content of a blob entry. -#[derive(Debug)] -pub struct ParsedBlob<'a> { - /// The blob record this blob came from. - pub blob_entry: BlobEntry<'a>, - - /// The parsed blob data. - pub blob: BlobData<'a>, -} - -impl<'a> ParsedBlob<'a> { - /// Compute the content digest of this blob using the specified hash type. - pub fn digest_with(&self, hash: DigestType) -> Result, AppleCodesignError> { - hash.digest_data(self.blob_entry.data) - } -} - -impl<'a> TryFrom> for ParsedBlob<'a> { - type Error = AppleCodesignError; - - fn try_from(blob_entry: BlobEntry<'a>) -> Result { - let blob = BlobData::from_blob_bytes(blob_entry.data)?; - - Ok(Self { blob_entry, blob }) - } -} - -/// Represents Apple's common embedded code signature data structures. -/// -/// This type represents a lightly parsed `SuperBlob` with [CodeSigningMagic::EmbeddedSignature]. -/// It is the most common embedded signature data format you are likely to encounter. -pub struct EmbeddedSignature<'a> { - /// Magic value from header. - pub magic: CodeSigningMagic, - /// Length of this super blob. - pub length: u32, - /// Number of blobs in this super blob. - pub count: u32, - - /// Raw data backing this super blob. - pub data: &'a [u8], - - /// All the blobs within this super blob. - pub blobs: Vec>, -} - -impl<'a> std::fmt::Debug for EmbeddedSignature<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.debug_struct("SuperBlob") - .field("magic", &self.magic) - .field("length", &self.length) - .field("count", &self.count) - .field("blobs", &self.blobs) - .finish() - } -} - -// There are other impl blocks for this structure in other modules. -impl<'a> EmbeddedSignature<'a> { - /// Attempt to parse an embedded signature super blob from data. - /// - /// The argument to this function is likely the subset of the - /// `__LINKEDIT` Mach-O section that the `LC_CODE_SIGNATURE` load instructions - /// points it. - pub fn from_bytes(data: &'a [u8]) -> Result { - let offset = &mut 0; - - // Parse the 3 fields from the SuperBlob. - let magic = data.gread_with::(offset, scroll::BE)?.into(); - - if magic != CodeSigningMagic::EmbeddedSignature { - return Err(AppleCodesignError::BadMagic( - "embedded signature super blob", - )); - } - - let length = data.gread_with(offset, scroll::BE)?; - let count = data.gread_with(offset, scroll::BE)?; - - // Following the SuperBlob header is an array of .count BlobIndex defining - // the Blob that follow. - // - // The BlobIndex doesn't declare the length of each Blob. However, it appears - // the first 8 bytes of each blob contain the u32 magic and u32 length. - // We do parse those here and set the blob length/slice accordingly. However, - // we take an extra level of precaution by first computing a slice that doesn't - // overrun into the next blob or past the end of the input buffer. This - // helps detect invalid length advertisements in the blob payload. - let mut blob_indices = Vec::with_capacity(count as usize); - for _ in 0..count { - blob_indices.push(data.gread_with::(offset, scroll::BE)?); - } - - let mut blobs = Vec::with_capacity(blob_indices.len()); - - for (i, index) in blob_indices.iter().enumerate() { - let end_offset = if i == blob_indices.len() - 1 { - data.len() - } else { - blob_indices[i + 1].offset as usize - }; - - let full_slice = &data[index.offset as usize..end_offset]; - let (magic, blob_length, _) = read_blob_header(full_slice)?; - - // Self-reported length can't be greater than the data we have. - let blob_data = match blob_length.cmp(&full_slice.len()) { - Ordering::Greater => { - return Err(AppleCodesignError::SuperblobMalformed); - } - Ordering::Equal => full_slice, - Ordering::Less => &full_slice[0..blob_length], - }; - - blobs.push(BlobEntry { - index: i, - slot: index.typ.into(), - offset: index.offset as usize, - magic: magic.into(), - length: blob_length, - data: blob_data, - }); - } - - Ok(Self { - magic, - length, - count, - data, - blobs, - }) - } - - /// Find the first occurrence of the specified slot. - pub fn find_slot(&self, slot: CodeSigningSlot) -> Option<&BlobEntry<'a>> { - self.blobs.iter().find(|e| e.slot == slot) - } - - pub fn find_slot_parsed( - &self, - slot: CodeSigningSlot, - ) -> Result>, AppleCodesignError> { - if let Some(entry) = self.find_slot(slot) { - Ok(Some(entry.clone().into_parsed_blob()?)) - } else { - Ok(None) - } - } - - /// Attempt to resolve the primary `CodeDirectoryBlob` for this signature data. - /// - /// Returns Err on data parsing error or if the blob slot didn't contain a code - /// directory. - /// - /// Returns `Ok(None)` if there is no code directory slot. - pub fn code_directory(&self) -> Result>>, AppleCodesignError> { - if let Some(parsed) = self.find_slot_parsed(CodeSigningSlot::CodeDirectory)? { - if let BlobData::CodeDirectory(cd) = parsed.blob { - Ok(Some(cd)) - } else { - Err(AppleCodesignError::BadMagic("code directory blob")) - } - } else { - Ok(None) - } - } - - /// Obtain code directories occupying alternative slots. - /// - /// Embedded signatures set aside a few slots for alternate code directory data structures. - /// This method will resolve any that are present. - pub fn alternate_code_directories( - &self, - ) -> Result>)>, AppleCodesignError> { - let slots = [ - CodeSigningSlot::AlternateCodeDirectory0, - CodeSigningSlot::AlternateCodeDirectory1, - CodeSigningSlot::AlternateCodeDirectory2, - CodeSigningSlot::AlternateCodeDirectory3, - CodeSigningSlot::AlternateCodeDirectory4, - ]; - - let mut res = vec![]; - - for slot in slots { - if let Some(parsed) = self.find_slot_parsed(slot)? { - if let BlobData::CodeDirectory(cd) = parsed.blob { - res.push((slot, cd)); - } else { - return Err(AppleCodesignError::BadMagic( - "wrong blob magic in alternative code directory slot", - )); - } - } - } - - Ok(res) - } - - /// Resolve all code directories in this signature. - pub fn all_code_directories( - &self, - ) -> Result>)>, AppleCodesignError> { - let mut res = vec![]; - - if let Some(cd) = self.code_directory()? { - res.push((CodeSigningSlot::CodeDirectory, cd)); - } - - res.extend(self.alternate_code_directories()?); - - Ok(res) - } - - /// Attempt to resolve a code directory containing digests of the specified type. - pub fn code_directory_for_digest( - &self, - digest: DigestType, - ) -> Result>>, AppleCodesignError> { - for (_, cd) in self.all_code_directories()? { - if cd.digest_type == digest { - return Ok(Some(cd)); - } - } - - Ok(None) - } - - /// Attempt to resolve a parsed [EntitlementsBlob] for this signature data. - /// - /// Returns Err on data parsing error or if the blob slot didn't contain an entitlments - /// blob. - /// - /// Returns `Ok(None)` if there is no entitlements slot. - pub fn entitlements(&self) -> Result>>, AppleCodesignError> { - if let Some(parsed) = self.find_slot_parsed(CodeSigningSlot::Entitlements)? { - if let BlobData::Entitlements(entitlements) = parsed.blob { - Ok(Some(entitlements)) - } else { - Err(AppleCodesignError::BadMagic("entitlements blob")) - } - } else { - Ok(None) - } - } - - /// Attempt to resolve a parsed [RequirementSetBlob] for this signature data. - /// - /// Returns Err on data parsing error or if the blob slot didn't contain a requirements - /// blob. - /// - /// Returns `Ok(None)` if there is no requirements slot. - pub fn code_requirements( - &self, - ) -> Result>>, AppleCodesignError> { - if let Some(parsed) = self.find_slot_parsed(CodeSigningSlot::RequirementSet)? { - if let BlobData::RequirementSet(reqs) = parsed.blob { - Ok(Some(reqs)) - } else { - Err(AppleCodesignError::BadMagic("requirements blob")) - } - } else { - Ok(None) - } - } - - /// Attempt to resolve raw CMS signature data. - /// - /// The returned data is likely DER PKCS#7 with the root object - /// pkcs7-signedData (1.2.840.113549.1.7.2). - pub fn signature_data(&self) -> Result, AppleCodesignError> { - if let Some(parsed) = self.find_slot(CodeSigningSlot::Signature) { - // Make sure it validates. - ParsedBlob::try_from(parsed.clone())?; - - Ok(Some(parsed.payload()?)) - } else { - Ok(None) - } - } - - /// Obtain the parsed CMS [SignedData]. - pub fn signed_data(&self) -> Result, AppleCodesignError> { - if let Some(data) = self.signature_data()? { - // Sometime we get an empty data slice. This has been observed on DMG signatures. - // In that scenario, pretend there is no CMS data at all. - if data.is_empty() { - Ok(None) - } else { - let signed_data = SignedData::parse_ber(data)?; - - Ok(Some(signed_data)) - } - } else { - Ok(None) - } - } -} diff --git a/apple-codesign/src/embedded_signature_builder.rs b/apple-codesign/src/embedded_signature_builder.rs deleted file mode 100644 index 5bf84a9d2..000000000 --- a/apple-codesign/src/embedded_signature_builder.rs +++ /dev/null @@ -1,342 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Provides primitives for constructing embeddable signature data structures. - -use { - crate::{ - code_directory::CodeDirectoryBlob, - embedded_signature::{ - create_superblob, Blob, BlobData, BlobWrapperBlob, CodeSigningMagic, CodeSigningSlot, - EmbeddedSignature, - }, - error::AppleCodesignError, - }, - bcder::{encode::PrimitiveContent, Oid}, - bytes::Bytes, - cryptographic_message_syntax::{asn1::rfc5652::OID_ID_DATA, SignedDataBuilder, SignerBuilder}, - log::{info, warn}, - reqwest::Url, - std::collections::BTreeMap, - x509_certificate::{ - rfc5652::AttributeValue, CapturedX509Certificate, DigestAlgorithm, KeyInfoSigner, - }, -}; - -/// OID for signed attribute containing plist of code directory digests. -/// -/// 1.2.840.113635.100.9.1. -pub const CD_DIGESTS_PLIST_OID: bcder::ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 9, 1]); - -/// OID for signed attribute containing the digests of code directories. -/// -/// 1.2.840.113635.100.9.2 -pub const CD_DIGESTS_OID: bcder::ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 9, 2]); - -#[derive(Clone, Copy, Debug, PartialEq)] -enum BlobsState { - Empty, - SpecialAdded, - CodeDirectoryAdded, - SignatureAdded, - TicketAdded, -} - -impl Default for BlobsState { - fn default() -> Self { - Self::Empty - } -} - -/// An entity for producing and writing [EmbeddedSignature]. -/// -/// This entity can be used to incrementally build up super blob data. -#[derive(Debug, Default)] -pub struct EmbeddedSignatureBuilder<'a> { - state: BlobsState, - blobs: BTreeMap>, -} - -impl<'a> EmbeddedSignatureBuilder<'a> { - /// Create a new instance suitable for stapling a notarization ticket. - /// - /// This starts with an existing [EmbeddedSignature] / superblob because stapling - /// a notarization ticket just adds a new ticket slot without modifying existing - /// slots. - pub fn new_for_stapling(signature: EmbeddedSignature<'a>) -> Result { - let blobs = signature - .blobs - .into_iter() - .map(|blob| { - let parsed = blob.into_parsed_blob()?; - - Ok((parsed.blob_entry.slot, parsed.blob)) - }) - .collect::, AppleCodesignError>>()?; - - Ok(Self { - state: BlobsState::CodeDirectoryAdded, - blobs, - }) - } - - /// Obtain the code directory registered with this instance. - pub fn code_directory(&self) -> Option<&CodeDirectoryBlob> { - self.blobs.get(&CodeSigningSlot::CodeDirectory).map(|blob| { - if let BlobData::CodeDirectory(cd) = blob { - (*cd).as_ref() - } else { - panic!("a non code directory should never be stored in the code directory slot"); - } - }) - } - - /// Register a blob into a slot. - /// - /// There can only be a single blob per slot. Last write wins. - /// - /// The code directory and embedded signature cannot be added using this method. - /// - /// Blobs cannot be registered after a code directory or signature are added, as this - /// would invalidate the signature. - pub fn add_blob( - &mut self, - slot: CodeSigningSlot, - blob: BlobData<'a>, - ) -> Result<(), AppleCodesignError> { - match self.state { - BlobsState::Empty | BlobsState::SpecialAdded => {} - BlobsState::CodeDirectoryAdded - | BlobsState::SignatureAdded - | BlobsState::TicketAdded => { - return Err(AppleCodesignError::SignatureBuilder( - "cannot add blobs after code directory or signature is registered", - )); - } - } - - if matches!( - blob, - BlobData::CodeDirectory(_) - | BlobData::EmbeddedSignature(_) - | BlobData::EmbeddedSignatureOld(_) - ) { - return Err(AppleCodesignError::SignatureBuilder( - "cannot register code directory or signature blob via add_blob()", - )); - } - - self.blobs.insert(slot, blob); - - self.state = BlobsState::SpecialAdded; - - Ok(()) - } - - /// Register a [CodeDirectoryBlob] with this builder. - /// - /// This is the recommended mechanism to register a Code Directory with this instance. - /// - /// When a code directory is registered, this method will automatically ensure digests - /// of previously registered blobs/slots are present in the code directory. This - /// removes the burden from callers of having to keep the code directory in sync with - /// other registered blobs. - /// - /// This function accepts the slot to add the code directory to because alternative - /// slots can be registered. - pub fn add_code_directory( - &mut self, - cd_slot: CodeSigningSlot, - mut cd: CodeDirectoryBlob<'a>, - ) -> Result<&CodeDirectoryBlob, AppleCodesignError> { - if matches!(self.state, BlobsState::SignatureAdded) { - return Err(AppleCodesignError::SignatureBuilder( - "cannot add code directory after signature data added", - )); - } - - for (slot, blob) in &self.blobs { - // Not all slots are expressible in the cd specials list! - if !slot.is_code_directory_specials_expressible() { - continue; - } - - let digest = blob.digest_with(cd.digest_type)?; - - cd.set_slot_digest(*slot, digest)?; - } - - self.blobs.insert(cd_slot, cd.into()); - self.state = BlobsState::CodeDirectoryAdded; - - Ok(self.code_directory().expect("we just inserted this key")) - } - - /// Add an alternative code directory. - /// - /// This is a wrapper for [Self::add_code_directory()] that has logic for determining the - /// appropriate slot for the code directory. - pub fn add_alternative_code_directory( - &mut self, - cd: CodeDirectoryBlob<'a>, - ) -> Result<&CodeDirectoryBlob, AppleCodesignError> { - let mut our_slot = CodeSigningSlot::AlternateCodeDirectory0; - - for slot in self.blobs.keys() { - if slot.is_alternative_code_directory() { - our_slot = CodeSigningSlot::from(u32::from(*slot) + 1); - - if !our_slot.is_alternative_code_directory() { - return Err(AppleCodesignError::SignatureBuilder( - "no more available alternative code directory slots", - )); - } - } - } - - self.add_code_directory(our_slot, cd) - } - - /// The a CMS signature and register its signature blob. - /// - /// `signing_key` and `signing_cert` denote the keypair being used to produce a - /// cryptographic signature. - /// - /// `time_stamp_url` is an optional time-stamp protocol server to use to record - /// the signature in. - /// - /// `certificates` are extra X.509 certificates to register in the signing chain. - /// - /// This method errors if called before a code directory is registered. - pub fn create_cms_signature( - &mut self, - signing_key: &dyn KeyInfoSigner, - signing_cert: &CapturedX509Certificate, - time_stamp_url: Option<&Url>, - certificates: impl Iterator, - ) -> Result<(), AppleCodesignError> { - let main_cd = self - .code_directory() - .ok_or(AppleCodesignError::SignatureBuilder( - "cannot create CMS signature unless code directory is present", - ))?; - - if let Some(cn) = signing_cert.subject_common_name() { - warn!("creating cryptographic signature with certificate {}", cn); - } - - let mut cdhashes = vec![]; - let mut attributes = vec![]; - - for (slot, blob) in &self.blobs { - if *slot == CodeSigningSlot::CodeDirectory || slot.is_alternative_code_directory() { - if let BlobData::CodeDirectory(cd) = blob { - // plist digests use the native digest of the code directory but always - // truncated at 20 bytes. - let mut digest = cd.digest_with(cd.digest_type)?; - digest.truncate(20); - cdhashes.push(plist::Value::Data(digest)); - - // ASN.1 values are a SEQUENCE of (OID, OctetString) with the native - // digest. - let digest = cd.digest_with(cd.digest_type)?; - let alg = DigestAlgorithm::try_from(cd.digest_type)?; - - attributes.push(AttributeValue::new(bcder::Captured::from_values( - bcder::Mode::Der, - bcder::encode::sequence(( - Oid::from(alg).encode_ref(), - bcder::OctetString::new(digest.into()).encode_ref(), - )), - ))); - } else { - return Err(AppleCodesignError::SignatureBuilder( - "unexpected blob type in code directory slot", - )); - } - } - } - - let mut plist_dict = plist::Dictionary::new(); - plist_dict.insert("cdhashes".to_string(), plist::Value::Array(cdhashes)); - - let mut plist_xml = vec![]; - plist::Value::from(plist_dict) - .to_writer_xml(&mut plist_xml) - .map_err(AppleCodesignError::CodeDirectoryPlist)?; - // We also need to include a trailing newline to conform with Apple's XML - // writer. - plist_xml.push(b'\n'); - - let signer = SignerBuilder::new(signing_key, signing_cert.clone()) - .message_id_content(main_cd.to_blob_bytes()?) - .signed_attribute_octet_string( - Oid(Bytes::copy_from_slice(CD_DIGESTS_PLIST_OID.as_ref())), - &plist_xml, - ); - - let signer = signer.signed_attribute(Oid(CD_DIGESTS_OID.as_ref().into()), attributes); - - let signer = if let Some(time_stamp_url) = time_stamp_url { - info!("Using time-stamp server {}", time_stamp_url); - signer.time_stamp_url(time_stamp_url.clone())? - } else { - signer - }; - - let der = SignedDataBuilder::default() - // The default is `signed-data`. But Apple appears to use the `data` content-type, - // in violation of RFC 5652 Section 5, which says `signed-data` should be - // used when there are signatures. - .content_type(Oid(OID_ID_DATA.as_ref().into())) - .signer(signer) - .certificates(certificates) - .build_der()?; - - self.blobs.insert( - CodeSigningSlot::Signature, - BlobData::BlobWrapper(Box::new(BlobWrapperBlob::from_data_owned(der))), - ); - self.state = BlobsState::SignatureAdded; - - Ok(()) - } - - /// Add notarization ticket data. - /// - /// This will register a new ticket slot holding the notarization ticket data. - pub fn add_notarization_ticket( - &mut self, - ticket_data: Vec, - ) -> Result<(), AppleCodesignError> { - self.blobs.insert( - CodeSigningSlot::Ticket, - BlobData::BlobWrapper(Box::new(BlobWrapperBlob::from_data_owned(ticket_data))), - ); - self.state = BlobsState::TicketAdded; - - Ok(()) - } - - /// Create the embedded signature "superblob" data. - pub fn create_superblob(&self) -> Result, AppleCodesignError> { - if matches!(self.state, BlobsState::Empty | BlobsState::SpecialAdded) { - return Err(AppleCodesignError::SignatureBuilder( - "code directory required in order to materialize superblob", - )); - } - - let blobs = self - .blobs - .iter() - .map(|(slot, blob)| { - let data = blob.to_blob_bytes()?; - - Ok((*slot, data)) - }) - .collect::, AppleCodesignError>>()?; - - create_superblob(CodeSigningMagic::EmbeddedSignature, blobs.iter()) - } -} diff --git a/apple-codesign/src/entitlements.rs b/apple-codesign/src/entitlements.rs deleted file mode 100644 index 9600d6a5e..000000000 --- a/apple-codesign/src/entitlements.rs +++ /dev/null @@ -1,556 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -/*! Code entitlements handling. */ - -use { - crate::{code_directory::ExecutableSegmentFlags, AppleCodesignError}, - plist::Value, - rasn::{ - ber::enc::{Encoder as DerEncoder, Error as DerError}, - enc::Error, - types::{Class, Tag}, - Encoder, - }, - std::collections::BTreeMap, -}; - -/// Encode a [Value] to DER, writing to an encoder. -fn der_encode_value(encoder: &mut DerEncoder, value: &Value) -> Result<(), DerError> { - match value { - Value::Boolean(v) => encoder.encode_bool(Tag::BOOL, *v), - Value::Integer(v) => { - let integer = rasn::types::Integer::from(v.as_signed().unwrap()); - encoder.encode_integer(Tag::INTEGER, &integer) - } - Value::String(string) => encoder.encode_utf8_string(Tag::UTF8_STRING, string), - Value::Array(array) => encoder.encode_sequence(Tag::SEQUENCE, |encoder| { - for v in array { - der_encode_value(encoder, v)?; - } - Ok(()) - }), - Value::Dictionary(dict) => { - // make sure it's sorted alphabetically - let map = dict.into_iter().collect::>(); - encoder.encode_sequence(Tag::new(Class::Context, 16), |encoder| { - for (k, v) in map { - encoder.encode_sequence(Tag::SEQUENCE, |encoder| { - encoder.encode_utf8_string(Tag::UTF8_STRING, k)?; - der_encode_value(encoder, v)?; - Ok(()) - })?; - } - Ok(()) - }) - } - - Value::Data(_) => Err(DerError::custom("encoding of data values not supported")), - Value::Date(_) => Err(DerError::custom("encoding of date values not supported")), - Value::Real(_) => Err(DerError::custom("encoding of real values not supported")), - Value::Uid(_) => Err(DerError::custom("encoding of uid values not supported")), - _ => Err(DerError::custom( - "encoding of unknown value type not supported", - )), - } -} - -/// Encode an entitlements plist to DER. -pub fn der_encode_entitlements_plist(value: &Value) -> Result, AppleCodesignError> { - rasn::der::encode_scope(|encoder| { - encoder.encode_sequence(Tag::new(Class::Application, 16), |encoder| { - encoder.encode_integer(Tag::INTEGER, &rasn::types::Integer::from(1))?; - der_encode_value(encoder, value)?; - Ok(()) - }) - }) - .map_err(|e| AppleCodesignError::EntitlementsDerEncode(format!("{}", e))) -} - -/// Convert an entitlements plist to [ExecutableSegmentFlags]. -/// -/// Some entitlements plist values imply features in executable segment flags. -/// This function resolves those implied features. -pub fn plist_to_executable_segment_flags(value: &Value) -> ExecutableSegmentFlags { - let mut flags = ExecutableSegmentFlags::empty(); - - if let Value::Dictionary(d) = value { - if matches!(d.get("get-task-allow"), Some(Value::Boolean(true))) { - flags |= ExecutableSegmentFlags::ALLOW_UNSIGNED; - } - if matches!(d.get("run-unsigned-code"), Some(Value::Boolean(true))) { - flags |= ExecutableSegmentFlags::ALLOW_UNSIGNED; - } - if matches!( - d.get("com.apple.private.cs.debugger"), - Some(Value::Boolean(true)) - ) { - flags |= ExecutableSegmentFlags::DEBUGGER; - } - if matches!(d.get("dynamic-codesigning"), Some(Value::Boolean(true))) { - flags |= ExecutableSegmentFlags::JIT; - } - if matches!( - d.get("com.apple.private.skip-library-validation"), - Some(Value::Boolean(true)) - ) { - flags |= ExecutableSegmentFlags::SKIP_LIBRARY_VALIDATION; - } - if matches!( - d.get("com.apple.private.amfi.can-load-cdhash"), - Some(Value::Boolean(true)) - ) { - flags |= ExecutableSegmentFlags::CAN_LOAD_CD_HASH; - } - if matches!( - d.get("com.apple.private.amfi.can-execute-cdhash"), - Some(Value::Boolean(true)) - ) { - flags |= ExecutableSegmentFlags::CAN_EXEC_CD_HASH; - } - } - - flags -} - -#[cfg(test)] -mod test { - use { - super::*, - crate::{ - embedded_signature::{Blob, CodeSigningSlot}, - macho::MachFile, - }, - anyhow::anyhow, - anyhow::Result, - plist::{Date, Uid}, - std::{ - process::Command, - time::{Duration, SystemTime}, - }, - }; - - const DER_EMPTY_DICT: &[u8] = &[112, 5, 2, 1, 1, 176, 0]; - const DER_BOOL_FALSE: &[u8] = &[ - 112, 15, 2, 1, 1, 176, 10, 48, 8, 12, 3, 107, 101, 121, 1, 1, 0, - ]; - const DER_BOOL_TRUE: &[u8] = &[ - 112, 15, 2, 1, 1, 176, 10, 48, 8, 12, 3, 107, 101, 121, 1, 1, 255, - ]; - const DER_INTEGER_0: &[u8] = &[ - 112, 15, 2, 1, 1, 176, 10, 48, 8, 12, 3, 107, 101, 121, 2, 1, 0, - ]; - const DER_INTEGER_NEG1: &[u8] = &[ - 112, 15, 2, 1, 1, 176, 10, 48, 8, 12, 3, 107, 101, 121, 2, 1, 255, - ]; - const DER_INTEGER_1: &[u8] = &[ - 112, 15, 2, 1, 1, 176, 10, 48, 8, 12, 3, 107, 101, 121, 2, 1, 1, - ]; - const DER_INTEGER_42: &[u8] = &[ - 112, 15, 2, 1, 1, 176, 10, 48, 8, 12, 3, 107, 101, 121, 2, 1, 42, - ]; - const DER_STRING_EMPTY: &[u8] = &[112, 14, 2, 1, 1, 176, 9, 48, 7, 12, 3, 107, 101, 121, 12, 0]; - const DER_STRING_VALUE: &[u8] = &[ - 112, 19, 2, 1, 1, 176, 14, 48, 12, 12, 3, 107, 101, 121, 12, 5, 118, 97, 108, 117, 101, - ]; - const DER_ARRAY_EMPTY: &[u8] = &[112, 14, 2, 1, 1, 176, 9, 48, 7, 12, 3, 107, 101, 121, 48, 0]; - const DER_ARRAY_FALSE: &[u8] = &[ - 112, 17, 2, 1, 1, 176, 12, 48, 10, 12, 3, 107, 101, 121, 48, 3, 1, 1, 0, - ]; - const DER_ARRAY_TRUE_FOO: &[u8] = &[ - 112, 22, 2, 1, 1, 176, 17, 48, 15, 12, 3, 107, 101, 121, 48, 8, 1, 1, 255, 12, 3, 102, 111, - 111, - ]; - const DER_DICT_EMPTY: &[u8] = &[ - 112, 14, 2, 1, 1, 176, 9, 48, 7, 12, 3, 107, 101, 121, 176, 0, - ]; - const DER_DICT_BOOL: &[u8] = &[ - 112, 26, 2, 1, 1, 176, 21, 48, 19, 12, 3, 107, 101, 121, 176, 12, 48, 10, 12, 5, 105, 110, - 110, 101, 114, 1, 1, 0, - ]; - const DER_MULTIPLE_KEYS: &[u8] = &[ - 112, 37, 2, 1, 1, 176, 32, 48, 8, 12, 3, 107, 101, 121, 1, 1, 0, 48, 9, 12, 4, 107, 101, - 121, 50, 1, 1, 255, 48, 9, 12, 4, 107, 101, 121, 51, 2, 1, 42, - ]; - - /// Signs a binary with custom entitlements XML and retrieves the entitlements DER. - /// - /// This uses Apple's `codesign` executable to sign the current binary then uses - /// our library for extracting the entitlements DER that it generated. - #[allow(unused)] - fn sign_and_get_entitlements_der(value: &Value) -> Result> { - let this_exe = std::env::current_exe()?; - - let temp_dir = tempfile::tempdir()?; - - let in_path = temp_dir.path().join("original"); - let entitlements_path = temp_dir.path().join("entitlements.xml"); - std::fs::copy(&this_exe, &in_path)?; - { - let mut fh = std::fs::File::create(&entitlements_path)?; - value.to_writer_xml(&mut fh)?; - } - - let args = vec![ - "--verbose".to_string(), - "--force".to_string(), - // ad-hoc signing since we don't care about a CMS signature. - "-s".to_string(), - "-".to_string(), - "--generate-entitlement-der".to_string(), - "--entitlements".to_string(), - format!("{}", entitlements_path.display()), - format!("{}", in_path.display()), - ]; - - let status = Command::new("codesign").args(args).output()?; - if !status.status.success() { - return Err(anyhow!("codesign invocation failure")); - } - - // Now extract the data from the Apple produced code signature. - - let signed_exe = std::fs::read(&in_path)?; - let mach = MachFile::parse(&signed_exe)?; - let macho = mach.nth_macho(0)?; - - let signature = macho - .code_signature()? - .expect("unable to find code signature"); - - let slot = signature - .find_slot(CodeSigningSlot::EntitlementsDer) - .expect("unable to find der entitlements blob"); - - match slot.clone().into_parsed_blob()?.blob { - crate::embedded_signature::BlobData::EntitlementsDer(der) => { - Ok(der.serialize_payload()?) - } - _ => Err(anyhow!( - "failed to obtain entitlements DER (this should never happen)" - )), - } - } - - // This test is failing in CI. Older versions of macOS / codesign likely have - // a different DER encoding mechanism. - // #[test] - #[cfg(target_os = "macos")] - #[allow(unused)] - fn apple_der_entitlements_encoding() -> Result<()> { - // `codesign` prints "unknown exception" if we attempt to serialize a plist where - // the root element isn't a dict. - let mut d = plist::Dictionary::new(); - - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_EMPTY_DICT - ); - - d.insert("key".into(), Value::Boolean(false)); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_BOOL_FALSE - ); - - d.insert("key".into(), Value::Boolean(true)); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_BOOL_TRUE - ); - - d.insert("key".into(), Value::Integer(0u32.into())); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_INTEGER_0 - ); - - d.insert("key".into(), Value::Integer((-1i32).into())); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_INTEGER_NEG1 - ); - - d.insert("key".into(), Value::Integer(1u32.into())); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_INTEGER_1 - ); - - d.insert("key".into(), Value::Integer(42u32.into())); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_INTEGER_42 - ); - - // Floats fail to encode to DER. - d.insert("key".into(), Value::Real(0.0f32.into())); - assert!(sign_and_get_entitlements_der(&Value::Dictionary(d.clone())).is_err()); - - d.insert("key".into(), Value::Real((-1.0f32).into())); - assert!(sign_and_get_entitlements_der(&Value::Dictionary(d.clone())).is_err()); - - d.insert("key".into(), Value::Real(1.0f32.into())); - assert!(sign_and_get_entitlements_der(&Value::Dictionary(d.clone())).is_err()); - - d.insert("key".into(), Value::String("".into())); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_STRING_EMPTY - ); - - d.insert("key".into(), Value::String("value".into())); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_STRING_VALUE - ); - - // Uids fail to encode with `UidNotSupportedInXmlPlist` message. - d.insert("key".into(), Value::Uid(Uid::new(0))); - assert!(sign_and_get_entitlements_der(&Value::Dictionary(d.clone())).is_err()); - - d.insert("key".into(), Value::Uid(Uid::new(1))); - assert!(sign_and_get_entitlements_der(&Value::Dictionary(d.clone())).is_err()); - - d.insert("key".into(), Value::Uid(Uid::new(42))); - assert!(sign_and_get_entitlements_der(&Value::Dictionary(d.clone())).is_err()); - - // Date doesn't appear to work due to - // `Failed to parse entitlements: AMFIUnserializeXML: syntax error near line 6`. Perhaps - // a bug in the plist crate? - d.insert( - "key".into(), - Value::Date(Date::from(SystemTime::UNIX_EPOCH)), - ); - assert!(sign_and_get_entitlements_der(&Value::Dictionary(d.clone())).is_err()); - d.insert( - "key".into(), - Value::Date(Date::from( - SystemTime::UNIX_EPOCH + Duration::from_secs(86400 * 365 * 30), - )), - ); - assert!(sign_and_get_entitlements_der(&Value::Dictionary(d.clone())).is_err()); - - // Data fails to encode to DER with `unknown exception`. - d.insert("key".into(), Value::Data(vec![])); - assert!(sign_and_get_entitlements_der(&Value::Dictionary(d.clone())).is_err()); - d.insert("key".into(), Value::Data(b"foo".to_vec())); - assert!(sign_and_get_entitlements_der(&Value::Dictionary(d.clone())).is_err()); - - d.insert("key".into(), Value::Array(vec![])); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_ARRAY_EMPTY - ); - - d.insert("key".into(), Value::Array(vec![Value::Boolean(false)])); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_ARRAY_FALSE - ); - - d.insert( - "key".into(), - Value::Array(vec![Value::Boolean(true), Value::String("foo".into())]), - ); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_ARRAY_TRUE_FOO - ); - - let mut inner = plist::Dictionary::new(); - d.insert("key".into(), Value::Dictionary(inner.clone())); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_DICT_EMPTY - ); - - inner.insert("inner".into(), Value::Boolean(false)); - d.insert("key".into(), Value::Dictionary(inner.clone())); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_DICT_BOOL - ); - - d.insert("key".into(), Value::Boolean(false)); - d.insert("key2".into(), Value::Boolean(true)); - d.insert("key3".into(), Value::Integer(42i32.into())); - assert_eq!( - sign_and_get_entitlements_der(&Value::Dictionary(d.clone()))?, - DER_MULTIPLE_KEYS - ); - - Ok(()) - } - - #[test] - fn der_encoding() -> Result<()> { - let mut d = plist::Dictionary::new(); - - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_EMPTY_DICT - ); - - d.insert("key".into(), Value::Boolean(false)); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_BOOL_FALSE - ); - - d.insert("key".into(), Value::Boolean(true)); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_BOOL_TRUE - ); - - d.insert("key".into(), Value::Integer(0u32.into())); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_INTEGER_0 - ); - - d.insert("key".into(), Value::Integer((-1i32).into())); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_INTEGER_NEG1 - ); - - d.insert("key".into(), Value::Integer(1u32.into())); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_INTEGER_1 - ); - - d.insert("key".into(), Value::Integer(42u32.into())); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_INTEGER_42 - ); - - d.insert("key".into(), Value::Real(0.0f32.into())); - assert!(matches!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone())), - Err(AppleCodesignError::EntitlementsDerEncode(_)) - )); - - d.insert("key".into(), Value::Real((-1.0f32).into())); - assert!(matches!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone())), - Err(AppleCodesignError::EntitlementsDerEncode(_)) - )); - - d.insert("key".into(), Value::Real(1.0f32.into())); - assert!(matches!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone())), - Err(AppleCodesignError::EntitlementsDerEncode(_)) - )); - - d.insert("key".into(), Value::String("".into())); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_STRING_EMPTY - ); - - d.insert("key".into(), Value::String("value".into())); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_STRING_VALUE - ); - - d.insert("key".into(), Value::Uid(Uid::new(0))); - assert!(matches!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone())), - Err(AppleCodesignError::EntitlementsDerEncode(_)) - )); - - d.insert("key".into(), Value::Uid(Uid::new(1))); - assert!(matches!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone())), - Err(AppleCodesignError::EntitlementsDerEncode(_)) - )); - - d.insert("key".into(), Value::Uid(Uid::new(42))); - assert!(matches!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone())), - Err(AppleCodesignError::EntitlementsDerEncode(_)) - )); - - d.insert( - "key".into(), - Value::Date(Date::from(SystemTime::UNIX_EPOCH)), - ); - assert!(matches!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone())), - Err(AppleCodesignError::EntitlementsDerEncode(_)) - )); - d.insert( - "key".into(), - Value::Date(Date::from( - SystemTime::UNIX_EPOCH + Duration::from_secs(86400 * 365 * 30), - )), - ); - assert!(matches!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone())), - Err(AppleCodesignError::EntitlementsDerEncode(_)) - )); - - // Data fails to encode to DER with `unknown exception`. - d.insert("key".into(), Value::Data(vec![])); - assert!(matches!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone())), - Err(AppleCodesignError::EntitlementsDerEncode(_)) - )); - d.insert("key".into(), Value::Data(b"foo".to_vec())); - assert!(matches!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone())), - Err(AppleCodesignError::EntitlementsDerEncode(_)) - )); - - d.insert("key".into(), Value::Array(vec![])); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_ARRAY_EMPTY - ); - - d.insert("key".into(), Value::Array(vec![Value::Boolean(false)])); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_ARRAY_FALSE - ); - - d.insert( - "key".into(), - Value::Array(vec![Value::Boolean(true), Value::String("foo".into())]), - ); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_ARRAY_TRUE_FOO - ); - - let mut inner = plist::Dictionary::new(); - d.insert("key".into(), Value::Dictionary(inner.clone())); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_DICT_EMPTY - ); - - inner.insert("inner".into(), Value::Boolean(false)); - d.insert("key".into(), Value::Dictionary(inner.clone())); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_DICT_BOOL - ); - - d.insert("key".into(), Value::Boolean(false)); - d.insert("key2".into(), Value::Boolean(true)); - d.insert("key3".into(), Value::Integer(42i32.into())); - assert_eq!( - der_encode_entitlements_plist(&Value::Dictionary(d.clone()))?, - DER_MULTIPLE_KEYS - ); - - Ok(()) - } -} diff --git a/apple-codesign/src/error.rs b/apple-codesign/src/error.rs deleted file mode 100644 index 61de69264..000000000 --- a/apple-codesign/src/error.rs +++ /dev/null @@ -1,362 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -use { - crate::remote_signing::RemoteSignError, - cryptographic_message_syntax::CmsError, - std::path::PathBuf, - thiserror::Error, - tugger_apple::UniversalMachOError, - x509_certificate::{KeyAlgorithm, X509CertificateError}, -}; - -/// Unified error type for Apple code signing. -#[derive(Debug, Error)] -pub enum AppleCodesignError { - #[error("unknown command")] - CliUnknownCommand, - - #[error("bad argument")] - CliBadArgument, - - #[error("{0}")] - CliGeneralError(String), - - #[error("I/O error: {0}")] - Io(#[from] std::io::Error), - - #[error("binary parsing error: {0}")] - Goblin(#[from] goblin::error::Error), - - #[error("invalid Mach-O binary: {0}")] - InvalidBinary(String), - - #[error("invalid binary index within Mach-O: {0}")] - InvalidMachOIndex(usize), - - #[error("binary does not have code signature data")] - BinaryNoCodeSignature, - - #[error("X.509 certificate handler error: {0}")] - X509(#[from] X509CertificateError), - - #[error("CMS error: {0}")] - Cms(#[from] CmsError), - - #[error("JSON serialization error: {0}")] - SerdeJson(#[from] serde_json::Error), - - #[error("YAML serialization error: {0}")] - SerdeYaml(#[from] serde_yaml::Error), - - #[error("glob error: {0}")] - GlobPattern(#[from] glob::PatternError), - - #[error("problems reported during verification")] - VerificationProblems, - - #[error("certificate error: {0}")] - CertificateGeneric(String), - - #[error("certificate decode error: {0}")] - CertificateDecode(bcder::decode::DecodeError), - - #[error("PEM error: {0}")] - CertificatePem(pem::PemError), - - #[error("X.509 certificate parsing error: {0}")] - X509Parse(String), - - #[error("unsupported key algorithm in certificate: {0:?}")] - CertificateUnsupportedKeyAlgorithm(KeyAlgorithm), - - #[error("unspecified cryptography error in certificate")] - CertificateRing(ring::error::Unspecified), - - #[error("bad string value in certificate: {0:?}")] - CertificateCharset(bcder::string::CharSetError), - - #[error("error parsing version string: {0}")] - VersionParse(#[from] semver::Error), - - #[error("JWT error: {0}")] - Jwt(#[from] jsonwebtoken::errors::Error), - - #[error("XAR error: {0}")] - Xar(#[from] apple_xar::Error), - - #[error("Apple flat package error: {0}")] - FlatPackage(#[from] apple_flat_package::Error), - - #[error("unable to locate __TEXT segment")] - MissingText, - - #[error("unable to locate __LINKEDIT segment")] - MissingLinkedit, - - #[error("bad header magic in {0}")] - BadMagic(&'static str), - - #[error("data structure parse error: {0}")] - Scroll(#[from] scroll::Error), - - #[error("error parsing plist XML: {0}")] - PlistParseXml(plist::Error), - - #[error("error serializing plist to XML: {0}")] - PlistSerializeXml(plist::Error), - - #[error("malformed identifier string in code directory")] - CodeDirectoryMalformedIdentifier, - - #[error("malformed team name string in code directory")] - CodeDirectoryMalformedTeam, - - #[error("plist error in code directory: {0}")] - CodeDirectoryPlist(plist::Error), - - #[error("SuperBlob data is malformed")] - SuperblobMalformed, - - #[error("specified path is not of a recognized type")] - UnrecognizedPathType, - - #[error("functionality not implemented: {0}")] - Unimplemented(&'static str), - - #[error("unknown code signature flag: {0}")] - CodeSignatureUnknownFlag(String), - - #[error("entitlements data not valid UTF-8: {0}")] - EntitlementsBadUtf8(std::str::Utf8Error), - - #[error("error when encoding entitlements to DER: {0}")] - EntitlementsDerEncode(String), - - #[error("unknown executable segment flag: {0}")] - ExecutableSegmentUnknownFlag(String), - - #[error("unknown code requirement opcode: {0}")] - RequirementUnknownOpcode(u32), - - #[error("unknown code requirement match expression: {0}")] - RequirementUnknownMatchExpression(u32), - - #[error("code requirement data malformed: {0}")] - RequirementMalformed(&'static str), - - #[error("plist error in code resources: {0}")] - ResourcesPlist(plist::Error), - - #[error("base64 error in code resources: {0}")] - ResourcesBase64(base64::DecodeError), - - #[error("plist parse error in code resources: {0}")] - ResourcesPlistParse(String), - - #[error("bad regular expression in code resources: {0}; {1}")] - ResourcesBadRegex(String, regex::Error), - - #[error("__LINKEDIT isn't final Mach-O segment")] - LinkeditNotLast, - - #[error("__LINKEDIT segment contains data after signature")] - DataAfterSignature, - - #[error("insufficient room to write code signature load command")] - LoadCommandNoRoom, - - #[error("error writing Mach-O: {0}")] - MachOWrite(String), - - #[error("no identifier string provided")] - NoIdentifier, - - #[error("no signing certificate")] - NoSigningCertificate, - - #[error("signature data too large (please report this issue)")] - SignatureDataTooLarge, - - #[error("invalid builder operation: {0}")] - SignatureBuilder(&'static str), - - #[error("HTTP error: {0}")] - Reqwest(#[from] reqwest::Error), - - #[error("unknown digest algorithm")] - DigestUnknownAlgorithm, - - #[error("unsupported digest algorithm")] - DigestUnsupportedAlgorithm, - - #[error("unspecified digest error")] - DigestUnspecified, - - #[error("error interfacing with directory-based bundle: {0}")] - DirectoryBundle(anyhow::Error), - - #[error("nested bundle does not exist: {0}")] - BundleUnknown(String), - - #[error("bundle Info.plist does not define CFBundleIdentifier: {0}")] - BundleNoIdentifier(PathBuf), - - #[error("bundle Info.plist does not define CFBundleExecutable: {0}")] - BundleNoMainExecutable(PathBuf), - - #[error( - "unexpected resource rule evaluation when signing nested bundle (please report this issue)" - )] - BundleUnexpectedResourceRuleResult, - - #[error("unable to parse settings scope: {0}")] - ParseSettingsScope(String), - - #[error("incorrect password given when decrypting PFX data")] - PfxBadPassword, - - #[error("error parsing PFX data: {0}")] - PfxParseError(String), - - #[cfg(target_os = "macos")] - #[error("SecurityFramework error: {0}")] - SecurityFramework(#[from] security_framework::base::Error), - - #[error("error interfacing with macOS keychain: {0}")] - KeychainError(String), - - #[error("failed to find certificate satisfying requirements: {0}")] - CertificateNotFound(String), - - #[error("the given OID does not match a recognized Apple certificate authority extension")] - OidIsntCertificateAuthority, - - #[error("the given OID does not match a recognized Apple extended key usage extension")] - OidIsntExtendedKeyUsage, - - #[error("the given OID does not match a recognized Apple code signing extension")] - OidIsntCodeSigningExtension, - - #[error("error building certificate: {0}")] - CertificateBuildError(String), - - #[error("unknown certificate profile: {0}")] - UnknownCertificateProfile(String), - - #[error("unknown code execution policy: {0}")] - UnknownPolicy(String), - - #[error("unable to generate code requirement policy: {0}")] - PolicyFormulationError(String), - - #[error("error producing universal Mach-O binary: {0}")] - UniversalMachO(#[from] UniversalMachOError), - - #[error("zip error: {0}")] - ZipError(#[from] zip::result::ZipError), - - #[error("error writing app metadata XML: {0}")] - AppMetadataXml(xml::writer::Error), - - #[error("error writing XML: {0}")] - XmlWrite(xml::writer::Error), - - #[error("signing XAR archives requires a signing certificate")] - XarNoAdhoc, - - #[error("App Store Connect API Key error: {0}")] - AppStoreConnectApiKey(String), - - #[error("Could not find App Store Connect API key in default search locations")] - AppStoreConnectApiKeyNotFound, - - #[error("do not know how to notarize {0}")] - NotarizeUnsupportedPath(PathBuf), - - #[error("no authentication credentials to perform notarization request")] - NotarizeNoAuthCredentials, - - #[error("reached time limit waiting for notarization to complete")] - NotarizeWaitLimitReached, - - #[error("error interacting with Notary API")] - NotarizeServerError, - - #[error("notarization rejected: StatusCode={0}; StatusMessage={1}")] - NotarizeRejected(i64, String), - - #[error("notarization is incomplete (no status code and message)")] - NotarizeIncomplete, - - #[error("notarization package is invalid")] - NotarizeInvalid, - - #[error("notarization record not in response: {0}")] - NotarizationRecordNotInResponse(String), - - #[error("signed ticket data not found in ticket lookup response (this should not happen)")] - NotarizationRecordNoSignedTicket, - - #[error("signedTicket in notarization ticket lookup response is not BYTES: {0}")] - NotarizationRecordSignedTicketNotBytes(String), - - #[error("notarization ticket lookup failure: {0}: {1}")] - NotarizationLookupFailure(String, String), - - #[error("error decoding base64 in notarization ticket: {0}")] - NotarizationRecordDecodeFailure(base64::DecodeError), - - #[error("unable to determine app platform from bundle")] - BundleUnknownAppPlatform, - - #[error("do not support stapling {0:?} bundles")] - StapleUnsupportedBundleType(apple_bundles::BundlePackageType), - - #[error("XAR file is malformed; cannot staple")] - StapleMalformedXar, - - #[error("failed to find main executable in bundle")] - StapleMainExecutableNotFound, - - #[error("do not know how to staple {0}")] - StapleUnsupportedPath(PathBuf), - - #[error("bad header magic in DMG; not a DMG file?")] - DmgBadMagic, - - #[error("cannot notarize DMG without an embedded signature")] - DmgNotarizeNoSignature, - - #[error("cannot staple DMG without an embedded signature")] - DmgStapleNoSignature, - - #[error("failed to find certificate in smartcard slot {0}")] - SmartcardNoCertificate(String), - - #[error("failed to authenticate with smartcard device")] - SmartcardFailedAuthentication, - - #[cfg(feature = "yubikey")] - #[error("YubiKey error: {0}")] - YubiKey(#[from] yubikey::Error), - - #[error("poisoned lock")] - PoisonedLock, - - #[error("internal API / logic error: {0}")] - LogicError(String), - - #[error("zip structs error: {0}")] - ZipStructs(#[from] zip_structs::zip_error::ZipReadError), - - #[error("remote signing error: {0}")] - RemoteSign(#[from] RemoteSignError), - - #[error("bytestream creation error: {0}")] - AwsByteStream(#[from] aws_smithy_http::byte_stream::Error), - - #[error("s3 upload error: {0}")] - AwsS3Error(#[from] aws_sdk_s3::Error), -} diff --git a/apple-codesign/src/lib.rs b/apple-codesign/src/lib.rs deleted file mode 100644 index 9fbaf9570..000000000 --- a/apple-codesign/src/lib.rs +++ /dev/null @@ -1,160 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Binary code signing for Apple platforms. -//! -//! This crate implements application code signing for Apple operating systems -//! (like macOS and iOS). A goal of this crate is to serve as a stand-in -//! replacement for Apple's `codesign` (and similar tools) without a dependency -//! on an Apple hardware device or operating system: you should be able to -//! sign and release Apple binaries from Linux, Windows, or other non-Apple -//! environments if you want to. -//! -//! Apple code signing is complex and there are likely several areas where -//! this crate and Apple's implementations don't align. It is highly recommended -//! to validate output against what Apple's official tools produce. -//! -//! # Features and Capabilities -//! -//! This crate can: -//! -//! * Find code signature data embedded in Mach-O binaries (both single and -//! multi-arch/fat/universal binaries). (See [MachOBinary] struct.) -//! * Deeply parse code signature data into Rust structs. (See -//! [EmbeddedSignature], [BlobData], and e.g. [CodeDirectoryBlob]. -//! * Parse and verify the RFC 5652 Cryptographic Message Syntax (CMS) -//! signature data. This includes using a Time-Stamp Protocol (TSP) / RFC 3161 -//! server for including a signed time-stamp token for that signature. -//! (Functionality provided by the `cryptographic-message-syntax` crate, -//! developed in the same repository as this crate.) -//! * Generate new embedded signature data, including cryptographically -//! signing that data using any signing key and X.509 certificate chain -//! you provide. (See [MachOSigner] and [BundleSigner].) -//! * Writing a new Mach-O file containing new signature data. (See -//! [MachOSigner].) -//! * Parse `CodeResources` XML plist files defining information on nested/signed -//! resources within bundles. This includes parsing and applying the filtering -//! rules defining in these files. -//! * Sign bundles. Nested bundles will automatically be signed. Additional -//! Mach-O binaries outside the main executable will also be signed. Non -//! Mach-O/code files will be digested. A `CodeResources` XML file will be -//! produced. -//! * Submit notarization requests to Apple and query notarization status. (Bundles, -//! DMGs, and `.pkg` installers are all supported.) -//! * Retrieve notarization tickets from Apple and staple. All formats supporting -//! notarization can be stapled. -//! -//! There are a number of missing features and capabilities from this crate -//! that we hope are eventually implemented: -//! -//! * No parsing of the Code Signing Requirements DSL. We support parsing the binary -//! requirements to Rust structs, serializing back to binary, and rendering to the -//! human friendly DSL. You will need to use the `csreq` tool to compile an -//! expression to binary and then give that binary blob to this crate. Alternatively, -//! you can write Rust code to construct a code requirements expression and serialize -//! that to binary. -//! * No turnkey support for signing keys. We want to make it easier for obtaining -//! signing keys (and their X.509 certificate chain) for use with this crate. It -//! should be possible to easily integrate with the OS's key store or hardware -//! based stores (such as Yubikeys). We also don't look for necessary X.509 -//! certificate extensions that Apple's verification likely mandates, which we should -//! do and enforce. -//! * Some more advanced bundles or `.pkg` files may not sign, notarize, or staple -//! correctly. Problems here are considered bugs and should be reported. -//! -//! There is missing features and functionality that will likely never be implemented: -//! -//! * Binary verification compliant with Apple's operating systems. We are capable -//! of verifying the digests of code and other embedded signature data. We can also -//! verify that a cryptographic signature came from the annotated public key in -//! that signature. We can also write heuristics to look for certain common problems -//! with signatures. But we can't and likely never will implement all the rules Apple -//! uses to verify a binary for execution because we perceive there to be little -//! value in doing this. This crate could be used to build such functionality -//! elsewhere, however. -//! -//! # End-User Documentation -//! -//! The end-user documentation is maintained as a Sphinx docs tree in the `docs` -//! directory. The latest version of the documentation is published at -//! . -//! -//! # Getting Started -//! -//! The [UnifiedSigner] type is a good place to start to see how the high level API for -//! signing is implemented. -//! -//! To learn about the low-level data structures in embedded code signatures, read -//! [specification]. Or look at the code in [embedded_signature] and -//! [embedded_signature_builder]. -//! -//! [MachOSigner] is the type responsible for signing Mach-O files. -//! -//! [BundleSigner] is the type responsible for signing bundles. -//! -//! [dmg::DmgSigner] signs DMG files. -//! -//! The [EmbeddedSignature] represents a parsed Apple code signature and provides API -//! for data retrieval. -//! -//! # Accessing Apple Code Signing Certificates -//! -//! This crate doesn't yet support integrating with the macOS keychain to obtain -//! or use the code signing certificate private key. However, it does support -//! importing the certificate key from a `.p12` file exported from the `Keychain -//! Access` application. It also supports exporting the x509 certificate chain -//! for a given certificate by speaking directly to the macOS keychain APIs. -//! -//! See the `keychain-export-certificate-chain` CLI command for exporting a -//! code signing certificate's x509 chain as PEM. - -mod apple_certificates; -pub use apple_certificates::*; -pub mod app_store_connect; -mod bundle_signing; -pub use bundle_signing::*; -mod certificate; -pub use certificate::*; -mod code_directory; -pub use code_directory::*; -pub mod code_requirement; -pub use code_requirement::*; -mod code_resources; -pub use code_resources::*; -pub mod cryptography; -pub mod dmg; -pub mod embedded_signature; -pub use embedded_signature::*; -pub mod embedded_signature_builder; -pub use embedded_signature_builder::*; -pub mod entitlements; -mod error; -pub use error::*; -mod macho; -pub use macho::*; -#[cfg(target_os = "macos")] -#[allow(non_upper_case_globals)] -mod macos; -#[cfg(target_os = "macos")] -pub use macos::*; -mod macho_signing; -pub use macho_signing::*; -pub mod notarization; -pub use notarization::*; -mod policy; -pub use policy::*; -mod reader; -pub use reader::*; -pub mod remote_signing; -mod signing_settings; -pub use signing_settings::*; -mod signing; -pub use signing::*; -pub mod specification; -pub mod stapling; -pub mod ticket_lookup; -mod verify; -pub use verify::*; -#[cfg(feature = "yubikey")] -pub mod yubikey; diff --git a/apple-codesign/src/macho.rs b/apple-codesign/src/macho.rs deleted file mode 100644 index ef908e0aa..000000000 --- a/apple-codesign/src/macho.rs +++ /dev/null @@ -1,858 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -/*! Mach-O primitives related to code signing - -Code signing data is embedded within the named `__LINKEDIT` segment of -the Mach-O binary. An `LC_CODE_SIGNATURE` load command in the Mach-O header -will point you at this data. See `find_signature_data()` for this logic. - -Within the `__LINKEDIT` segment is a superblob defining embedded signature -data. -*/ - -use { - crate::{ - embedded_signature::{DigestType, EmbeddedSignature}, - error::AppleCodesignError, - signing_settings::{SettingsScope, SigningSettings}, - }, - cryptographic_message_syntax::time_stamp_message_http, - goblin::mach::{ - constants::{SEG_LINKEDIT, SEG_TEXT}, - header::MH_EXECUTE, - load_command::{ - CommandVariant, LinkeditDataCommand, LC_BUILD_VERSION, SIZEOF_LINKEDIT_DATA_COMMAND, - }, - parse_magic_and_ctx, Mach, MachO, - }, - rayon::prelude::*, - scroll::Pread, - x509_certificate::DigestAlgorithm, -}; - -/// A Mach-O binary. -pub struct MachOBinary<'a> { - /// Index within a fat binary this Mach-O resides at. - /// - /// If `None`, this is not inside a fat binary. - pub index: Option, - - /// The parsed Mach-O binary. - pub macho: MachO<'a>, - - /// The raw data backing the Mach-O binary. - pub data: &'a [u8], -} - -impl<'a> MachOBinary<'a> { - /// Parse a non-universal Mach-O binary from raw data. - pub fn parse(data: &'a [u8]) -> Result { - let macho = MachO::parse(data, 0)?; - - Ok(Self { - index: None, - macho, - data, - }) - } -} - -impl<'a> MachOBinary<'a> { - /// Attempt to extract a reference to raw signature data in a Mach-O binary. - /// - /// An `LC_CODE_SIGNATURE` load command in the Mach-O file header points to - /// signature data in the `__LINKEDIT` segment. - /// - /// This function is used as part of parsing signature data. You probably want to - /// use a function that parses referenced data. - pub fn find_signature_data( - &self, - ) -> Result>, AppleCodesignError> { - if let Some(linkedit_data_command) = - self.macho.load_commands.iter().find_map(|load_command| { - if let CommandVariant::CodeSignature(command) = &load_command.command { - Some(command) - } else { - None - } - }) - { - // Now find the slice of data in the __LINKEDIT segment we need to parse. - let (linkedit_segment_index, linkedit) = self - .macho - .segments - .iter() - .enumerate() - .find(|(_, segment)| { - if let Ok(name) = segment.name() { - name == SEG_LINKEDIT - } else { - false - } - }) - .ok_or(AppleCodesignError::MissingLinkedit)?; - - let linkedit_segment_start_offset = linkedit.fileoff as usize; - let linkedit_segment_end_offset = linkedit_segment_start_offset + linkedit.data.len(); - let linkedit_signature_start_offset = linkedit_data_command.dataoff as usize; - let linkedit_signature_end_offset = - linkedit_signature_start_offset + linkedit_data_command.datasize as usize; - let signature_start_offset = - linkedit_data_command.dataoff as usize - linkedit.fileoff as usize; - let signature_end_offset = - signature_start_offset + linkedit_data_command.datasize as usize; - - let signature_data = &linkedit.data[signature_start_offset..signature_end_offset]; - - Ok(Some(MachOSignatureData { - linkedit_segment_index, - linkedit_segment_start_offset, - linkedit_segment_end_offset, - linkedit_signature_start_offset, - linkedit_signature_end_offset, - signature_start_offset, - signature_end_offset, - linkedit_segment_data: linkedit.data, - signature_data, - })) - } else { - Ok(None) - } - } - - /// Obtain the code signature in the entity. - /// - /// Returns `Ok(None)` if no signature exists, `Ok(Some)` if it does, or - /// `Err` if there is a parse error. - pub fn code_signature(&self) -> Result, AppleCodesignError> { - if let Some(signature) = self.find_signature_data()? { - Ok(Some(EmbeddedSignature::from_bytes( - signature.signature_data, - )?)) - } else { - Ok(None) - } - } - - /// Determine the start and end offset of the executable segment of a binary. - pub fn executable_segment_boundary(&self) -> Result<(u64, u64), AppleCodesignError> { - let segment = self - .macho - .segments - .iter() - .find(|segment| matches!(segment.name(), Ok(SEG_TEXT))) - .ok_or_else(|| AppleCodesignError::InvalidBinary("no __TEXT segment".into()))?; - - Ok((segment.fileoff, segment.fileoff + segment.data.len() as u64)) - } - - /// Whether this is an executable Mach-O file. - pub fn is_executable(&self) -> bool { - self.macho.header.filetype == MH_EXECUTE - } - - /// The start offset of the code signature data within the __LINKEDIT segment. - pub fn code_signature_linkedit_start_offset(&self) -> Option { - let segment = self - .macho - .segments - .iter() - .find(|segment| matches!(segment.name(), Ok(SEG_LINKEDIT))); - - if let (Some(segment), Some(command)) = (segment, self.code_signature_load_command()) { - Some((command.dataoff as u64 - segment.fileoff) as u32) - } else { - None - } - } - - /// The end offset of the code signature data within the __LINKEDIT segment. - pub fn code_signature_linkedit_end_offset(&self) -> Option { - let start_offset = self.code_signature_linkedit_start_offset()?; - - self.code_signature_load_command() - .map(|command| start_offset + command.datasize) - } - - /// The byte offset within the binary at which point "code" stops. - /// - /// If a signature is present, this is the offset of the start of the - /// signature. Else it represents the end of the binary. - pub fn code_limit_binary_offset(&self) -> Result { - let last_segment = self - .macho - .segments - .last() - .ok_or(AppleCodesignError::MissingLinkedit)?; - if !matches!(last_segment.name(), Ok(SEG_LINKEDIT)) { - return Err(AppleCodesignError::LinkeditNotLast); - } - - if let Some(offset) = self.code_signature_linkedit_start_offset() { - Ok(last_segment.fileoff + offset as u64) - } else { - Ok(last_segment.fileoff + last_segment.data.len() as u64) - } - } - - /// Obtain __LINKEDIT segment data before the signature data. - /// - /// If there is no signature, returns all the data for the __LINKEDIT segment. - pub fn linkedit_data_before_signature(&self) -> Option<&[u8]> { - let segment = self - .macho - .segments - .iter() - .find(|segment| matches!(segment.name(), Ok(SEG_LINKEDIT))); - - if let Some(segment) = segment { - if let Some(offset) = self.code_signature_linkedit_start_offset() { - Some(&segment.data[0..offset as usize]) - } else { - Some(segment.data) - } - } else { - None - } - } - - /// Obtain Mach-O binary data to be digested in code digests. - /// - /// Returns the raw data whose digests will be captured by the Code Directory code digests. - pub fn digested_code_data(&self) -> Result<&[u8], AppleCodesignError> { - let code_limit = self.code_limit_binary_offset()?; - - Ok(&self.data[0..code_limit as _]) - } - - /// Obtain the size in bytes of all code digests given a digest type and page size. - pub fn code_digests_size( - &self, - digest: DigestType, - page_size: usize, - ) -> Result { - let empty = digest.digest_data(b"")?; - - Ok(self.digested_code_data()?.chunks(page_size).count() * empty.len()) - } - - /// Compute digests over code in this binary. - pub fn code_digests( - &self, - digest: DigestType, - page_size: usize, - ) -> Result>, AppleCodesignError> { - let data = self.digested_code_data()?; - - // Premature parallelism can be slower due to overhead of having to spin up threads. - // So only do parallel digests if we have enough data to warrant it. - if data.len() > 64 * 1024 * 1024 { - data.par_chunks(page_size) - .map(|c| digest.digest_data(c)) - .collect::, AppleCodesignError>>() - } else { - self.digested_code_data()? - .chunks(page_size) - .map(|chunk| digest.digest_data(chunk)) - .collect::, AppleCodesignError>>() - } - } - - /// Resolve the load command for the code signature. - pub fn code_signature_load_command(&self) -> Option { - self.macho.load_commands.iter().find_map(|lc| { - if let CommandVariant::CodeSignature(command) = lc.command { - Some(command) - } else { - None - } - }) - } - - /// Attempt to locate embedded Info.plist data. - pub fn embedded_info_plist(&self) -> Result>, AppleCodesignError> { - // Mach-O binaries can have the Info.plist data in an `__info_plist` section - // within the __TEXT segment. - for segment in &self.macho.segments { - if matches!(segment.name(), Ok(SEG_TEXT)) { - for (section, data) in segment.sections()? { - if matches!(section.name(), Ok("__info_plist")) { - return Ok(Some(data.to_vec())); - } - } - } - } - - Ok(None) - } - - /// Determines whether this crate is capable of signing a given Mach-O binary. - /// - /// Code in this crate is limited in the amount of Mach-O binary manipulation - /// it can perform (supporting rewriting all valid Mach-O binaries effectively - /// requires low-level awareness of all Mach-O constructs in order to perform - /// offset manipulation). This function can be used to test signing - /// compatibility. - /// - /// We currently only support signing Mach-O files already containing an - /// embedded signature. Often linked binaries automatically contain an embedded - /// signature containing just the code directory (without a cryptographically - /// signed signature), so this limitation hopefully isn't impactful. - pub fn check_signing_capability(&self) -> Result<(), AppleCodesignError> { - let last_segment = self - .macho - .segments - .iter() - .last() - .ok_or(AppleCodesignError::MissingLinkedit)?; - - // Last segment needs to be __LINKEDIT so we don't have to write offsets. - if !matches!(last_segment.name(), Ok(SEG_LINKEDIT)) { - return Err(AppleCodesignError::LinkeditNotLast); - } - - // Rules: - // - // 1. If there is an existing signature, there must be no data in - // the binary after it. (We don't know how to update references to - // other data to reflect offset changes.) - // 2. If there isn't an existing signature, there must be "room" between - // the last load command and the first section to write a new load - // command for the signature. - - if let Some(offset) = self.code_signature_linkedit_end_offset() { - if offset as usize == last_segment.data.len() { - Ok(()) - } else { - Err(AppleCodesignError::DataAfterSignature) - } - } else { - let last_load_command = self - .macho - .load_commands - .iter() - .last() - .ok_or_else(|| AppleCodesignError::InvalidBinary("no load commands".into()))?; - - let first_section = self - .macho - .segments - .iter() - .map(|segment| segment.sections()) - .collect::, _>>()? - .into_iter() - .flatten() - .next() - .ok_or_else(|| AppleCodesignError::InvalidBinary("no sections".into()))?; - - let load_commands_end_offset = - last_load_command.offset + last_load_command.command.cmdsize(); - - if first_section.0.offset as usize - load_commands_end_offset - >= SIZEOF_LINKEDIT_DATA_COMMAND - { - Ok(()) - } else { - Err(AppleCodesignError::LoadCommandNoRoom) - } - } - } - - /// Estimate the size in bytes of an embedded code signature. - pub fn estimate_embedded_signature_size( - &self, - settings: &SigningSettings, - ) -> Result { - let code_directory_count = 1 + settings - .extra_digests(SettingsScope::Main) - .map(|x| x.len()) - .unwrap_or_default(); - - // Assume the common data structures are 1024 bytes. - let mut size = 1024 * code_directory_count; - - // Reserve room for the code digests, which are proportional to binary size. - // We could avoid doing the actual digesting work here. But until people - // complain, don't worry about it. - size += self.code_digests_size(*settings.digest_type(), 4096)?; - - if let Some(digests) = settings.extra_digests(SettingsScope::Main) { - for digest in digests { - size += self.code_digests_size(*digest, 4096)?; - } - } - - // Assume the CMS data will take a fixed size. - if settings.signing_key().is_some() { - size += 4096; - } - - // Long certificate chains could blow up the size. Account for those. - for cert in settings.certificate_chain() { - size += cert.constructed_data().len(); - } - - // Obtain an actual timestamp token of placeholder data and use its length. - // This may be excessive to actually query the time-stamp server and issue - // a token. But these operations should be "cheap." - if let Some(timestamp_url) = settings.time_stamp_url() { - let message = b"deadbeef".repeat(32); - - if let Ok(response) = - time_stamp_message_http(timestamp_url.clone(), &message, DigestAlgorithm::Sha256) - { - if response.is_success() { - if let Some(l) = response.token_content_size() { - size += l; - } else { - size += 8192; - } - } else { - size += 8192; - } - } else { - size += 8192; - } - } - - // Align on 1k boundaries just because. - size += 1024 - size % 1024; - - Ok(size) - } - - /// Attempt to resolve the mach-o targeting settings. - pub fn find_targeting(&self) -> Result, AppleCodesignError> { - let ctx = parse_magic_and_ctx(self.data, 0)? - .1 - .expect("context should have been parsed before"); - - for lc in &self.macho.load_commands { - if lc.command.cmd() == LC_BUILD_VERSION { - let build_version = self - .data - .pread_with::(lc.offset, ctx.le)?; - - return Ok(Some(MachoTarget { - platform: build_version.platform.into(), - minimum_os_version: parse_version_nibbles(build_version.minos), - sdk_version: parse_version_nibbles(build_version.sdk), - })); - } - } - - for lc in &self.macho.load_commands { - let command = match lc.command { - CommandVariant::VersionMinMacosx(c) => Some((c, Platform::MacOs)), - CommandVariant::VersionMinIphoneos(c) => Some((c, Platform::IOs)), - CommandVariant::VersionMinTvos(c) => Some((c, Platform::TvOs)), - CommandVariant::VersionMinWatchos(c) => Some((c, Platform::WatchOs)), - _ => None, - }; - - if let Some((command, platform)) = command { - return Ok(Some(MachoTarget { - platform, - minimum_os_version: parse_version_nibbles(command.version), - sdk_version: parse_version_nibbles(command.sdk), - })); - } - } - - Ok(None) - } -} - -/// Describes signature data embedded within a Mach-O binary. -pub struct MachOSignatureData<'a> { - /// Which segment offset is the `__LINKEDIT` segment. - pub linkedit_segment_index: usize, - - /// Start offset of `__LINKEDIT` segment within the binary. - pub linkedit_segment_start_offset: usize, - - /// End offset of `__LINKEDIT` segment within the binary. - pub linkedit_segment_end_offset: usize, - - /// Start offset of signature data in `__LINKEDIT` within the binary. - pub linkedit_signature_start_offset: usize, - - /// End offset of signature data in `__LINKEDIT` within the binary. - pub linkedit_signature_end_offset: usize, - - /// The start offset of the signature data within the `__LINKEDIT` segment. - pub signature_start_offset: usize, - - /// The end offset of the signature data within the `__LINKEDIT` segment. - pub signature_end_offset: usize, - - /// Raw data in the `__LINKEDIT` segment. - pub linkedit_segment_data: &'a [u8], - - /// The signature data within the `__LINKEDIT` segment. - pub signature_data: &'a [u8], -} - -/// Content of an `LC_BUILD_VERSION` load command. -#[derive(Clone, Debug, Pread)] -pub struct BuildVersionCommand { - /// LC_BUILD_VERSION - pub cmd: u32, - /// Size of load command data. - /// - /// sizeof(self) + self.ntools * sizeof(BuildToolsVersion) - pub cmdsize: u32, - /// Platform identifier. - pub platform: u32, - /// Minimum operating system version. - /// - /// X.Y.Z encoded in nibbles as xxxx.yy.zz. - pub minos: u32, - /// SDK version. - /// - /// X.Y.Z encoded in nibbles as xxxx.yy.zz. - pub sdk: u32, - /// Number of tools entries following this structure. - pub ntools: u32, -} - -/// Represents `PLATFORM_` mach-o constants. -pub enum Platform { - MacOs, - IOs, - TvOs, - WatchOs, - BridgeOs, - MacCatalyst, - IosSimulator, - TvOsSimulator, - WatchOsSimulator, - DriverKit, - Unknown(u32), -} - -impl std::fmt::Display for Platform { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::MacOs => f.write_str("macOS"), - Self::IOs => f.write_str("iOS"), - Self::TvOs => f.write_str("tvOS"), - Self::WatchOs => f.write_str("watchOS"), - Self::BridgeOs => f.write_str("bridgeOS"), - Self::MacCatalyst => f.write_str("macCatalyst"), - Self::IosSimulator => f.write_str("iOSSimulator"), - Self::TvOsSimulator => f.write_str("tvOSSimulator"), - Self::WatchOsSimulator => f.write_str("watchOSSimulator"), - Self::DriverKit => f.write_str("driverKit"), - Self::Unknown(v) => f.write_fmt(format_args!("Unknown ({})", v)), - } - } -} - -impl From for Platform { - fn from(v: u32) -> Self { - match v { - 1 => Self::MacOs, - 2 => Self::IOs, - 3 => Self::TvOs, - 4 => Self::WatchOs, - 5 => Self::BridgeOs, - 6 => Self::MacCatalyst, - 7 => Self::IosSimulator, - 8 => Self::TvOsSimulator, - 9 => Self::WatchOsSimulator, - 10 => Self::DriverKit, - _ => Self::Unknown(v), - } - } -} - -impl Platform { - /// Resolve SHA-256 digest/signatures support for a given platform type. - pub fn sha256_digest_support(&self) -> Result { - let version = match self { - // macOS 10.11.4 introduced support for SHA-256. - Self::MacOs => ">=10.11.4", - // 11.0+ support SHA-256. - Self::IOs | Self::TvOs => ">=11.0.0", - // WatchOS always uses SHA-1 it appears. - Self::WatchOs => ">9999", - // Assume no platform needs SHA-1. - Self::Unknown(0) => ">9999", - // Assume everything else is new and supports SHA-256. - _ => "*", - }; - - Ok(semver::VersionReq::parse(version)?) - } -} - -/// Targeting settings for a Mach-O binary. -pub struct MachoTarget { - /// The OS/platform being targeted. - pub platform: Platform, - /// Minimum required OS version. - pub minimum_os_version: semver::Version, - /// SDK version targeting. - pub sdk_version: semver::Version, -} - -/// Parses and integer with nibbles xxxx.yy.zz into a [semver::Version]. -pub fn parse_version_nibbles(v: u32) -> semver::Version { - let major = v >> 16; - let minor = v << 16 >> 24; - let patch = v & 0xff; - - semver::Version::new(major as _, minor as _, patch as _) -} - -/// Convert a [semver::Version] to a u32 with nibble encoding used by Mach-O. -pub fn semver_to_macho_target_version(version: &semver::Version) -> u32 { - let major = version.major as u32; - let minor = version.minor as u32; - let patch = version.patch as u32; - - (major << 16) | ((minor & 0xff) << 8) | (patch & 0xff) -} - -/// Represents a semi-parsed Mach[-O] binary. -pub struct MachFile<'a> { - data: &'a [u8], - - machos: Vec>, -} - -impl<'a> MachFile<'a> { - /// Construct an instance from data. - pub fn parse(data: &'a [u8]) -> Result { - let mach = Mach::parse(data)?; - - let machos = match mach { - Mach::Binary(macho) => vec![MachOBinary { - index: None, - macho, - data, - }], - Mach::Fat(multiarch) => { - let mut machos = vec![]; - - for (index, arch) in multiarch.arches()?.into_iter().enumerate() { - let macho = multiarch.get(index)?; - - machos.push(MachOBinary { - index: Some(index), - macho, - data: arch.slice(data), - }); - } - - machos - } - }; - - Ok(Self { data, machos }) - } - - /// Whether this Mach-O data has multiple architectures. - pub fn is_fat(&self) -> bool { - self.machos.len() > 1 - } - - /// Iterate [MachO] instances in this data. - /// - /// The `Option` is `Some` if this is a universal Mach-O or `None` otherwise. - pub fn iter_macho(&self) -> impl Iterator { - self.machos.iter() - } - - pub fn nth_macho(&self, index: usize) -> Result<&MachOBinary<'a>, AppleCodesignError> { - Ok(self - .machos - .get(index) - .ok_or_else(|| AppleCodesignError::InvalidMachOIndex(index))?) - } - - /// Produce an iterator over each [MachOBinary], consuming self. - pub fn into_iter(self) -> impl Iterator> { - self.machos.into_iter() - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::embedded_signature::Blob, - std::{ - io::Read, - path::{Path, PathBuf}, - }, - }; - - const MACHO_UNIVERSAL_MAGIC: [u8; 4] = [0xca, 0xfe, 0xba, 0xbe]; - const MACHO_64BIT_MAGIC: [u8; 4] = [0xfe, 0xed, 0xfa, 0xcf]; - - /// Find files in a directory appearing to be Mach-O by sniffing magic. - /// - /// Ignores file I/O errors. - fn find_likely_macho_files(path: &Path) -> Vec { - let mut res = Vec::new(); - - let dir = std::fs::read_dir(path).unwrap(); - - for entry in dir { - let entry = entry.unwrap(); - - if let Ok(mut fh) = std::fs::File::open(&entry.path()) { - let mut magic = [0; 4]; - - if let Ok(size) = fh.read(&mut magic) { - if size == 4 && (magic == MACHO_UNIVERSAL_MAGIC || magic == MACHO_64BIT_MAGIC) { - res.push(entry.path()); - } - } - } - } - - res - } - - fn find_apple_embedded_signature<'a>(macho: &'a MachOBinary) -> Option> { - if let Ok(Some(signature)) = macho.code_signature() { - Some(signature) - } else { - None - } - } - - fn validate_macho(path: &Path, macho: &MachOBinary) { - // We found signature data in the binary. - if let Some(signature) = find_apple_embedded_signature(macho) { - // Attempt a deep parse of all blobs. - for blob in &signature.blobs { - match blob.clone().into_parsed_blob() { - Ok(parsed) => { - // Attempt to roundtrip the blob data. - match parsed.blob.to_blob_bytes() { - Ok(serialized) => { - if serialized != blob.data { - println!("blob serialization roundtrip failure on {}: index {}, magic {:?}", - path.display(), - blob.index, - blob.magic, - ); - } - } - Err(e) => { - println!( - "blob serialization failure on {}; index {}, magic {:?}: {:?}", - path.display(), - blob.index, - blob.magic, - e - ); - } - } - } - Err(e) => { - println!( - "blob parse failure on {}; index {}, magic {:?}: {:?}", - path.display(), - blob.index, - blob.magic, - e - ); - } - } - } - - // Found a CMS signed data blob. - if matches!(signature.signature_data(), Ok(Some(_))) { - match signature.signed_data() { - Ok(Some(signed_data)) => { - for signer in signed_data.signers() { - if let Err(e) = signer.verify_signature_with_signed_data(&signed_data) { - println!( - "signature verification failed for {}: {}", - path.display(), - e - ); - } - - if let Ok(()) = - signer.verify_message_digest_with_signed_data(&signed_data) - { - println!( - "message digest verification unexpectedly correct for {}", - path.display() - ); - } - } - } - Ok(None) => { - panic!("this shouln't happen (validated signature data is present"); - } - Err(e) => { - println!("error performing CMS parse of {}: {:?}", path.display(), e); - } - } - } - } - } - - fn validate_macho_in_dir(dir: &Path) { - for path in find_likely_macho_files(dir).into_iter() { - if let Ok(file_data) = std::fs::read(&path) { - if let Ok(mach) = MachFile::parse(&file_data) { - for macho in mach.into_iter() { - validate_macho(&path, &macho); - } - } - } - } - } - - #[test] - fn parse_applications_macho_signatures() { - // This test scans common directories containing Mach-O files on macOS and - // verifies we can parse CMS blobs within. - - if let Ok(dir) = std::fs::read_dir("/Applications") { - for entry in dir { - let entry = entry.unwrap(); - - let search_dir = entry.path().join("Contents").join("MacOS"); - - if search_dir.exists() { - validate_macho_in_dir(&search_dir); - } - } - } - - for dir in &["/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"] { - let dir = PathBuf::from(dir); - - if dir.exists() { - validate_macho_in_dir(&dir); - } - } - } - - #[test] - fn version_nibbles() { - assert_eq!( - parse_version_nibbles(12 << 16 | 1 << 8 | 2), - semver::Version::new(12, 1, 2) - ); - assert_eq!( - parse_version_nibbles(11 << 16 | 10 << 8 | 15), - semver::Version::new(11, 10, 15) - ); - assert_eq!( - semver_to_macho_target_version(&semver::Version::new(12, 1, 2)), - 12 << 16 | 1 << 8 | 2 - ); - } -} diff --git a/apple-codesign/src/macho_signing.rs b/apple-codesign/src/macho_signing.rs deleted file mode 100644 index 1c18c788b..000000000 --- a/apple-codesign/src/macho_signing.rs +++ /dev/null @@ -1,664 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Signing mach-o binaries. -//! -//! This module contains code for signing mach-o binaries. - -use { - crate::{ - code_directory::{CodeDirectoryBlob, CodeSignatureFlags, ExecutableSegmentFlags}, - code_requirement::{CodeRequirementExpression, CodeRequirements, RequirementType}, - embedded_signature::{ - BlobData, CodeSigningSlot, Digest, EntitlementsBlob, EntitlementsDerBlob, - RequirementSetBlob, - }, - embedded_signature_builder::EmbeddedSignatureBuilder, - entitlements::plist_to_executable_segment_flags, - error::AppleCodesignError, - macho::{semver_to_macho_target_version, MachFile, MachOBinary}, - policy::derive_designated_requirements, - signing_settings::{DesignatedRequirementMode, SettingsScope, SigningSettings}, - }, - goblin::mach::{ - constants::{SEG_LINKEDIT, SEG_PAGEZERO}, - load_command::{ - CommandVariant, LinkeditDataCommand, SegmentCommand32, SegmentCommand64, - LC_CODE_SIGNATURE, SIZEOF_LINKEDIT_DATA_COMMAND, - }, - parse_magic_and_ctx, - }, - log::{debug, info, warn}, - scroll::{ctx::SizeWith, IOwrite}, - std::{borrow::Cow, cmp::Ordering, collections::HashMap, io::Write, path::Path}, - tugger_apple::create_universal_macho, -}; - -/// Derive a new Mach-O binary with new signature data. -fn create_macho_with_signature( - macho: &MachOBinary, - signature_data: &[u8], -) -> Result, AppleCodesignError> { - // This should have already been called. But we do it again out of paranoia. - macho.check_signing_capability()?; - - // The assumption made by checking_signing_capability() is that signature data - // is at the end of the __LINKEDIT segment. So the replacement segment is the - // existing segment truncated at the signature start followed by the new signature - // data. - let new_linkedit_segment_size = macho - .linkedit_data_before_signature() - .ok_or(AppleCodesignError::MissingLinkedit)? - .len() - + signature_data.len(); - - // `codesign` rounds up the segment's vmsize to the nearest 16kb boundary. - // We emulate that behavior. - let remainder = new_linkedit_segment_size % 16384; - let new_linkedit_segment_vmsize = if remainder == 0 { - new_linkedit_segment_size - } else { - new_linkedit_segment_size + 16384 - remainder - }; - - assert!(new_linkedit_segment_vmsize >= new_linkedit_segment_size); - assert_eq!(new_linkedit_segment_vmsize % 16384, 0); - - let mut cursor = std::io::Cursor::new(Vec::::new()); - - // Mach-O data structures are variable endian. So use the endian defined - // by the magic when writing. - let ctx = parse_magic_and_ctx(macho.data, 0)? - .1 - .expect("context should have been parsed before"); - - // If there isn't a code signature presently, we'll need to introduce a load - // command for it. - let mut header = macho.macho.header; - if macho.code_signature_load_command().is_none() { - header.ncmds += 1; - header.sizeofcmds += SIZEOF_LINKEDIT_DATA_COMMAND as u32; - } - - cursor.iowrite_with(header, ctx)?; - - // Following the header are load commands. We need to update load commands - // to reflect changes to the signature size and __LINKEDIT segment size. - - let mut seen_signature_load_command = false; - - for load_command in &macho.macho.load_commands { - let original_command_data = - &macho.data[load_command.offset..load_command.offset + load_command.command.cmdsize()]; - - let written_len = match &load_command.command { - CommandVariant::CodeSignature(command) => { - seen_signature_load_command = true; - - let mut command = *command; - command.datasize = signature_data.len() as _; - - cursor.iowrite_with(command, ctx.le)?; - - LinkeditDataCommand::size_with(&ctx.le) - } - CommandVariant::Segment32(segment) => { - let segment = match segment.name() { - Ok(SEG_LINKEDIT) => { - let mut segment = *segment; - segment.filesize = new_linkedit_segment_size as _; - segment.vmsize = new_linkedit_segment_vmsize as _; - - segment - } - _ => *segment, - }; - - cursor.iowrite_with(segment, ctx.le)?; - - SegmentCommand32::size_with(&ctx.le) - } - CommandVariant::Segment64(segment) => { - let segment = match segment.name() { - Ok(SEG_LINKEDIT) => { - let mut segment = *segment; - segment.filesize = new_linkedit_segment_size as _; - segment.vmsize = new_linkedit_segment_vmsize as _; - - segment - } - _ => *segment, - }; - - cursor.iowrite_with(segment, ctx.le)?; - - SegmentCommand64::size_with(&ctx.le) - } - _ => { - // Reflect the original bytes. - cursor.write_all(original_command_data)?; - original_command_data.len() - } - }; - - // For the commands we mutated ourselves, there may be more data after the - // load command header. Write it out if present. - cursor.write_all(&original_command_data[written_len..])?; - } - - // If we didn't see a signature load command, write one out now. - if !seen_signature_load_command { - let command = LinkeditDataCommand { - cmd: LC_CODE_SIGNATURE, - cmdsize: SIZEOF_LINKEDIT_DATA_COMMAND as _, - dataoff: macho.code_limit_binary_offset()? as _, - datasize: signature_data.len() as _, - }; - - cursor.iowrite_with(command, ctx.le)?; - } - - // Write out segments, updating the __LINKEDIT segment when we encounter it. - for segment in macho.macho.segments.iter() { - // The initial __PAGEZERO segment contains no data (it is the magic and load - // commands) and overlaps with the __TEXT segment, which has .fileoff =0, so - // we ignore it. - if matches!(segment.name(), Ok(SEG_PAGEZERO)) { - continue; - } - - match cursor.position().cmp(&segment.fileoff) { - // Mach-O segments may have padding between them. In this case, copy these - // bytes (presumably NULLs but that isn't guaranteed) to the output. - Ordering::Less => { - let padding = &macho.data[cursor.position() as usize..segment.fileoff as usize]; - debug!( - "copying {} bytes outside segment boundaries before segment {}", - padding.len(), - segment.name().unwrap_or("") - ); - cursor.write_all(&padding)?; - } - // The __TEXT segment usually has .fileoff = 0, which has it overlapping with - // already written data. Allow this special case through. - Ordering::Greater if segment.fileoff == 0 => {} - - // The writer has overran into this segment. That means we screwed up on a - // previous loop iteration. - Ordering::Greater => { - return Err(AppleCodesignError::MachOWrite(format!( - "Mach-O segment corruption: cursor at 0x{:x} but segment begins at 0x{:x} (please report this bug)", - cursor.position(), - segment.fileoff - ))); - } - Ordering::Equal => {} - } - - assert!(segment.fileoff == 0 || segment.fileoff == cursor.position()); - - match segment.name() { - Ok(SEG_LINKEDIT) => { - cursor.write_all( - macho - .linkedit_data_before_signature() - .expect("__LINKEDIT segment data should resolve"), - )?; - cursor.write_all(signature_data)?; - } - _ => { - // At least the __TEXT segment has .fileoff = 0, which has it - // overlapping with already written data. So only write segment - // data new to the writer. - if segment.fileoff < cursor.position() { - if segment.data.is_empty() { - continue; - } - let remaining = - &segment.data[cursor.position() as usize..segment.filesize as usize]; - cursor.write_all(remaining)?; - } else { - cursor.write_all(segment.data)?; - } - } - } - } - - Ok(cursor.into_inner()) -} - -/// Write Mach-O file content to an output file. -pub fn write_macho_file( - input_path: &Path, - output_path: &Path, - macho_data: &[u8], -) -> Result<(), AppleCodesignError> { - // Read permissions first in case we overwrite the original file. - let permissions = std::fs::metadata(input_path)?.permissions(); - - if let Some(parent) = output_path.parent() { - std::fs::create_dir_all(parent)?; - } - - { - let mut fh = std::fs::File::create(output_path)?; - fh.write_all(macho_data)?; - } - - std::fs::set_permissions(output_path, permissions)?; - - Ok(()) -} - -/// Mach-O binary signer. -/// -/// This type provides a high-level interface for signing Mach-O binaries. -/// It handles parsing and rewriting Mach-O binaries and contains most of the -/// functionality for producing signatures for individual Mach-O binaries. -/// -/// Signing of both single architecture and fat/universal binaries is supported. -/// -/// # Circular Dependency -/// -/// There is a circular dependency between the generation of the Code Directory -/// present in the embedded signature and the Mach-O binary. See the note -/// in [crate::specification] for the gory details. The tl;dr is the Mach-O -/// data up to the signature data needs to be digested. But that digested data -/// contains load commands that reference the signature data and its size, which -/// can't be known until the Code Directory, CMS blob, and SuperBlob are all -/// created. -/// -/// Our solution to this problem is to estimate the size of the embedded -/// signature data and then pad the unused data will 0s. -pub struct MachOSigner<'data> { - /// Parsed Mach-O binaries. - machos: Vec>, -} - -impl<'data> MachOSigner<'data> { - /// Construct a new instance from unparsed data representing a Mach-O binary. - /// - /// The data will be parsed as a Mach-O binary (either single arch or fat/universal) - /// and validated that we are capable of signing it. - pub fn new(macho_data: &'data [u8]) -> Result { - let machos = MachFile::parse(macho_data)?.into_iter().collect::>(); - - Ok(Self { machos }) - } - - /// Write signed Mach-O data to the given writer using signing settings. - pub fn write_signed_binary( - &self, - settings: &SigningSettings, - writer: &mut impl Write, - ) -> Result<(), AppleCodesignError> { - // Implementing a true streaming writer requires calculating final sizes - // of all binaries so fat header offsets and sizes can be written first. We take - // the easy road and buffer individual Mach-O binaries internally. - - let binaries = self - .machos - .iter() - .enumerate() - .map(|(index, original_macho)| { - info!("signing Mach-O binary at index {}", index); - let settings = - settings.as_nested_macho_settings(index, original_macho.macho.header.cputype()); - - let signature_len = original_macho.estimate_embedded_signature_size(&settings)?; - - // Derive an intermediate Mach-O with placeholder NULLs for signature - // data so Code Directory digests over the load commands are correct. - let placeholder_signature_data = b"\0".repeat(signature_len); - - let intermediate_macho_data = - create_macho_with_signature(original_macho, &placeholder_signature_data)?; - - // A nice side-effect of this is that it catches bugs if we write malformed Mach-O! - let intermediate_macho = MachOBinary::parse(&intermediate_macho_data)?; - - let mut signature_data = self.create_superblob(&settings, &intermediate_macho)?; - info!("total signature size: {} bytes", signature_data.len()); - - // The Mach-O writer adjusts load commands based on the signature length. So pad - // with NULLs to get to our placeholder length. - match signature_data.len().cmp(&placeholder_signature_data.len()) { - Ordering::Greater => { - return Err(AppleCodesignError::SignatureDataTooLarge); - } - Ordering::Equal => {} - Ordering::Less => { - signature_data.extend_from_slice( - &b"\0".repeat(placeholder_signature_data.len() - signature_data.len()), - ); - } - } - - create_macho_with_signature(&intermediate_macho, &signature_data) - }) - .collect::, AppleCodesignError>>()?; - - if binaries.len() > 1 { - create_universal_macho(writer, binaries.iter().map(|x| x.as_slice()))?; - } else { - writer.write_all(&binaries[0])?; - } - - Ok(()) - } - - /// Create data constituting the SuperBlob to be embedded in the `__LINKEDIT` segment. - /// - /// The superblob contains the code directory, any extra blobs, and an optional - /// CMS structure containing a cryptographic signature. - /// - /// This takes an explicit Mach-O to operate on due to a circular dependency - /// between writing out the Mach-O and digesting its content. See the note - /// in [MachOSigner] for details. - pub fn create_superblob( - &self, - settings: &SigningSettings, - macho: &MachOBinary, - ) -> Result, AppleCodesignError> { - let mut builder = EmbeddedSignatureBuilder::default(); - - for (slot, blob) in self.create_special_blobs(settings, macho.is_executable())? { - builder.add_blob(slot, blob)?; - } - - let code_directory = self.create_code_directory(settings, macho)?; - info!("code directory version: {}", code_directory.version); - - builder.add_code_directory(CodeSigningSlot::CodeDirectory, code_directory)?; - - if let Some(digests) = settings.extra_digests(SettingsScope::Main) { - for digest_type in digests { - // Since everything consults settings for the digest to use, just make a new settings - // with a different digest. - let mut alt_settings = settings.clone(); - alt_settings.set_digest_type(*digest_type); - - info!( - "adding alternative code directory using digest {:?}", - digest_type - ); - let cd = self.create_code_directory(&alt_settings, macho)?; - - builder.add_alternative_code_directory(cd)?; - } - } - - if let Some((signing_key, signing_cert)) = settings.signing_key() { - builder.create_cms_signature( - signing_key, - signing_cert, - settings.time_stamp_url(), - settings.certificate_chain().iter().cloned(), - )?; - } - - builder.create_superblob() - } - - /// Create the `CodeDirectory` for the current configuration. - /// - /// This takes an explicit Mach-O to operate on due to a circular dependency - /// between writing out the Mach-O and digesting its content. See the note - /// in [MachOSigner] for details. - pub fn create_code_directory( - &self, - settings: &SigningSettings, - macho: &MachOBinary, - ) -> Result, AppleCodesignError> { - // TODO support defining or filling in proper values for fields with - // static values. - - let target = macho.find_targeting()?; - - if let Some(target) = &target { - info!( - "binary targets {} >= {} with SDK {}", - target.platform, target.minimum_os_version, target.sdk_version, - ); - } - - let mut flags = CodeSignatureFlags::empty(); - - if let Some(additional) = settings.code_signature_flags(SettingsScope::Main) { - info!( - "adding code signature flags from signing settings: {:?}", - additional - ); - flags |= additional; - } - - // The adhoc flag is set when there is no CMS signature. - if settings.signing_key().is_none() { - info!("creating ad-hoc signature"); - flags |= CodeSignatureFlags::ADHOC; - } else if flags.contains(CodeSignatureFlags::ADHOC) { - info!("removing ad-hoc code signature flag"); - flags -= CodeSignatureFlags::ADHOC; - } - - // Remove linker signed flag because we're not a linker. - if flags.contains(CodeSignatureFlags::LINKER_SIGNED) { - info!("removing linker signed flag from code signature (we're not a linker)"); - flags -= CodeSignatureFlags::LINKER_SIGNED; - } - - // Code limit fields hold the file offset at which code digests stop. This - // is the file offset in the `__LINKEDIT` segment when the embedded signature - // SuperBlob begins. - let (code_limit, code_limit_64) = match macho.code_limit_binary_offset()? { - x if x > u32::MAX as u64 => (0, Some(x)), - x => (x as u32, None), - }; - - let platform = 0; - let page_size = 4096u32; - - let (exec_seg_base, exec_seg_limit) = macho.executable_segment_boundary()?; - let (exec_seg_base, exec_seg_limit) = (Some(exec_seg_base), Some(exec_seg_limit)); - - // Executable segment flags are wonky. - // - // Foremost, these flags are only present if the Mach-O binary is an executable. So not - // matter what the settings say, we don't set these flags unless the Mach-O file type - // is proper. - // - // Executable segment flags are also derived from an associated entitlements plist. - let exec_seg_flags = if macho.is_executable() { - if let Some(entitlements) = settings.entitlements_plist(SettingsScope::Main) { - let flags = plist_to_executable_segment_flags(entitlements); - - if !flags.is_empty() { - info!("entitlements imply executable segment flags: {:?}", flags); - } - - Some(flags | ExecutableSegmentFlags::MAIN_BINARY) - } else { - Some(ExecutableSegmentFlags::MAIN_BINARY) - } - } else { - None - }; - - // The runtime version is the SDK version from the targeting loader commands. Same - // u32 with nibbles encoding the version. - // - // If the runtime code signature flag is set, we also need to set the runtime version - // or else the activation of the hardened runtime is incomplete. - - // If the settings defines a runtime version override, use it. - let runtime = match settings.runtime_version(SettingsScope::Main) { - Some(version) => { - info!( - "using hardened runtime version {} from signing settings", - version - ); - Some(semver_to_macho_target_version(version)) - } - None => None, - }; - - // If we still don't have a runtime but need one, derive from the target SDK. - let runtime = if runtime.is_none() && flags.contains(CodeSignatureFlags::RUNTIME) { - if let Some(target) = &target { - info!( - "using hardened runtime version {} derived from SDK version", - target.sdk_version - ); - Some(semver_to_macho_target_version(&target.sdk_version)) - } else { - warn!("hardened runtime version required but unable to derive suitable version; signature will likely fail Apple checks"); - None - } - } else { - runtime - }; - - let code_hashes = macho - .code_digests(*settings.digest_type(), page_size as _)? - .into_iter() - .map(|v| Digest { data: v.into() }) - .collect::>(); - - let mut special_hashes = HashMap::new(); - - // There is no corresponding blob for the info plist data since it is provided - // externally to the embedded signature. - if let Some(data) = settings.info_plist_data(SettingsScope::Main) { - special_hashes.insert( - CodeSigningSlot::Info, - Digest { - data: settings.digest_type().digest_data(data)?.into(), - }, - ); - } - - // There is no corresponding blob for resources data since it is provided - // externally to the embedded signature. - if let Some(data) = settings.code_resources_data(SettingsScope::Main) { - special_hashes.insert( - CodeSigningSlot::ResourceDir, - Digest { - data: settings.digest_type().digest_data(data)?.into(), - } - .to_owned(), - ); - } - - let ident = Cow::Owned( - settings - .binary_identifier(SettingsScope::Main) - .ok_or(AppleCodesignError::NoIdentifier)? - .to_string(), - ); - - let team_name = settings.team_id().map(|x| Cow::Owned(x.to_string())); - - let mut cd = CodeDirectoryBlob { - flags, - code_limit, - digest_size: settings.digest_type().hash_len()? as u8, - digest_type: *settings.digest_type(), - platform, - page_size, - code_limit_64, - exec_seg_base, - exec_seg_limit, - exec_seg_flags, - runtime, - ident, - team_name, - code_digests: code_hashes, - ..Default::default() - }; - - for (slot, digest) in special_hashes { - cd.set_slot_digest(slot, digest)?; - } - - cd.adjust_version(target); - cd.clear_newer_fields(); - - Ok(cd) - } - - /// Create blobs that need to be written given the current configuration. - /// - /// This emits all blobs except `CodeDirectory` and `Signature`, which are - /// special since they are derived from the blobs emitted here. - /// - /// The goal of this function is to emit data to facilitate the creation of - /// a `CodeDirectory`, which requires hashing blobs. - pub fn create_special_blobs( - &self, - settings: &SigningSettings, - is_executable: bool, - ) -> Result)>, AppleCodesignError> { - let mut res = Vec::new(); - - let mut requirements = CodeRequirements::default(); - - match settings.designated_requirement(SettingsScope::Main) { - DesignatedRequirementMode::Auto => { - // If we are using an Apple-issued cert, this should automatically - // derive appropriate designated requirements. - if let Some((_, cert)) = settings.signing_key() { - info!("attempting to derive code requirements from signing certificate"); - let identifier = Some( - settings - .binary_identifier(SettingsScope::Main) - .ok_or(AppleCodesignError::NoIdentifier)? - .to_string(), - ); - - if let Some(expr) = derive_designated_requirements(cert, identifier)? { - requirements.push(expr); - } - } - } - DesignatedRequirementMode::Explicit(exprs) => { - info!("using provided code requirements"); - for expr in exprs { - requirements.push(CodeRequirementExpression::from_bytes(expr)?.0); - } - } - } - - if !requirements.is_empty() { - info!("code requirements: {}", requirements); - - let mut blob = RequirementSetBlob::default(); - requirements.add_to_requirement_set(&mut blob, RequirementType::Designated)?; - - res.push((CodeSigningSlot::RequirementSet, blob.into())); - } - - if let Some(entitlements) = settings.entitlements_xml(SettingsScope::Main)? { - info!("adding entitlements XML"); - let blob = EntitlementsBlob::from_string(&entitlements); - - res.push((CodeSigningSlot::Entitlements, blob.into())); - } - - // The DER encoded entitlements weren't always present in the signature. The feature - // appears to have been introduced in macOS 10.14 and is the default behavior as of - // macOS 12 "when signing for all platforms." `codesign` appears to add the DER - // representation whenever entitlements are present, but only if the current binary is - // an executable (.filetype == MH_EXECUTE). - if is_executable { - if let Some(value) = settings.entitlements_plist(SettingsScope::Main) { - info!("adding entitlements DER"); - let blob = EntitlementsDerBlob::from_plist(value)?; - - res.push((CodeSigningSlot::EntitlementsDer, blob.into())); - } - } - - Ok(res) - } -} diff --git a/apple-codesign/src/macos.rs b/apple-codesign/src/macos.rs deleted file mode 100644 index 0fed2adb2..000000000 --- a/apple-codesign/src/macos.rs +++ /dev/null @@ -1,334 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Functionality that only works on macOS. - -use { - crate::{ - certificate::{AppleCertificate, OID_USER_ID}, - cryptography::PrivateKey, - error::AppleCodesignError, - remote_signing::{session_negotiation::PublicKeyPeerDecrypt, RemoteSignError}, - }, - bcder::Oid, - bytes::Bytes, - log::{error, warn}, - security_framework::{ - certificate::SecCertificate, - item::{ItemClass, ItemSearchOptions, Reference, SearchResult}, - key::{Algorithm as KeychainAlgorithm, SecKey}, - os::macos::{ - item::ItemSearchOptionsExt, - keychain::{SecKeychain, SecPreferencesDomain}, - }, - }, - signature::Signer, - std::ops::Deref, - x509_certificate::{ - CapturedX509Certificate, KeyAlgorithm, KeyInfoSigner, Sign, Signature, SignatureAlgorithm, - X509CertificateError, - }, -}; - -const SYSTEM_ROOTS_KEYCHAIN: &str = "/System/Library/Keychains/SystemRootCertificates.keychain"; - -/// A wrapper around [SecPreferencesDomain] so we can use crate local types. -#[derive(Clone, Copy, Debug)] -pub enum KeychainDomain { - User, - System, - Common, - Dynamic, -} - -impl From for SecPreferencesDomain { - fn from(v: KeychainDomain) -> Self { - match v { - KeychainDomain::User => Self::User, - KeychainDomain::System => Self::System, - KeychainDomain::Common => Self::Common, - KeychainDomain::Dynamic => Self::Dynamic, - } - } -} - -impl TryFrom<&str> for KeychainDomain { - type Error = String; - - fn try_from(v: &str) -> Result { - match v { - "user" => Ok(Self::User), - "system" => Ok(Self::System), - "common" => Ok(Self::Common), - "dynamic" => Ok(Self::Dynamic), - _ => Err(format!( - "{} is not a valid keychain domain; use user, system, common, or dynamic", - v - )), - } - } -} - -/// A certificate in a keychain. -#[derive(Clone)] -pub struct KeychainCertificate { - sec_cert: SecCertificate, - sec_key: SecKey, - captured: CapturedX509Certificate, -} - -impl Deref for KeychainCertificate { - type Target = CapturedX509Certificate; - - fn deref(&self) -> &Self::Target { - &self.captured - } -} - -impl Signer for KeychainCertificate { - fn try_sign(&self, message: &[u8]) -> Result { - let algorithm = self - .signature_algorithm() - .map_err(signature::Error::from_source)?; - - let algorithm = match algorithm { - SignatureAlgorithm::RsaSha1 => KeychainAlgorithm::RSASignatureMessagePKCS1v15SHA1, - SignatureAlgorithm::RsaSha256 => KeychainAlgorithm::RSASignatureMessagePKCS1v15SHA256, - SignatureAlgorithm::RsaSha384 => KeychainAlgorithm::RSASignatureMessagePKCS1v15SHA384, - SignatureAlgorithm::RsaSha512 => KeychainAlgorithm::RSASignatureMessagePKCS1v15SHA512, - SignatureAlgorithm::EcdsaSha256 => KeychainAlgorithm::ECDSASignatureMessageX962SHA256, - SignatureAlgorithm::EcdsaSha384 => KeychainAlgorithm::ECDSASignatureMessageX962SHA384, - SignatureAlgorithm::Ed25519 => KeychainAlgorithm::ECDSASignatureMessageX962SHA512, - }; - - warn!( - "attempting to create signature using keychain item: {}", - self.sec_cert.subject_summary() - ); - - let signature = self - .sec_key - .create_signature(algorithm, message) - .map_err(|e| { - signature::Error::from_source(format!( - "when attempting to create signature from keychain item: {}", - e - )) - })?; - - Ok(Signature::from(signature)) - } -} - -impl Sign for KeychainCertificate { - fn sign(&self, message: &[u8]) -> Result<(Vec, SignatureAlgorithm), X509CertificateError> { - let algorithm = self.signature_algorithm()?; - - Ok((self.try_sign(message)?.into(), algorithm)) - } - - fn key_algorithm(&self) -> Option { - self.captured.key_algorithm() - } - - fn public_key_data(&self) -> Bytes { - self.captured.public_key_data() - } - - fn signature_algorithm(&self) -> Result { - Ok(self.captured.signature_algorithm().ok_or( - X509CertificateError::UnknownSignatureAlgorithm(format!( - "{:?}", - self.captured.signature_algorithm_oid() - )), - )?) - } - - fn private_key_data(&self) -> Option> { - None - } - - fn rsa_primes(&self) -> Result, Vec)>, X509CertificateError> { - Ok(None) - } -} - -impl KeyInfoSigner for KeychainCertificate {} - -impl PublicKeyPeerDecrypt for KeychainCertificate { - fn decrypt(&self, _ciphertext: &[u8]) -> Result, RemoteSignError> { - // It doesn't look like the Rust bindings expose the APIs we need to - // implement decryption. Sadness. Will probably need to contribute - // those upstream... - error!("missing feature along with workarounds tracked in https://github.com/indygreg/PyOxidizer/issues/554"); - Err(RemoteSignError::Crypto( - "decryption not yet implemented for keychain stored keys".into(), - )) - } -} - -impl PrivateKey for KeychainCertificate { - fn as_key_info_signer(&self) -> &dyn KeyInfoSigner { - self - } - - fn to_public_key_peer_decrypt( - &self, - ) -> Result, AppleCodesignError> { - Ok(Box::new(self.clone())) - } - - fn finish(&self) -> Result<(), AppleCodesignError> { - Ok(()) - } -} - -impl KeychainCertificate { - /// Obtain a new [CapturedX509Certificate] for this item. - pub fn as_captured_x509_certificate(&self) -> CapturedX509Certificate { - self.captured.clone() - } -} - -fn find_certificates( - keychains: &[SecKeychain], -) -> Result, AppleCodesignError> { - let mut search = ItemSearchOptions::default(); - search.keychains(keychains); - // We fetch identities here because that gives us access to both the public - // cert and private key. The keychain doesn't need to be unlocked to get a - // handle on the private key: only when an operation on the private key is - // requested. - search.class(ItemClass::identity()); - search.limit(i32::MAX as i64); - - let mut certs = vec![]; - - for item in search.search()? { - match item { - SearchResult::Ref(reference) => match reference { - Reference::Identity(identity) => { - let cert = identity.certificate()?; - let private_key = identity.private_key()?; - - if let Ok(captured) = CapturedX509Certificate::from_der(cert.to_der()) { - certs.push(KeychainCertificate { - sec_cert: cert, - sec_key: private_key, - captured, - }); - } - } - - _ => { - return Err(AppleCodesignError::KeychainError( - "non-certificate reference from keychain search (this should not happen)" - .to_string(), - )); - } - }, - _ => { - return Err(AppleCodesignError::KeychainError( - "non-reference result from keychain search (this should not happen)" - .to_string(), - )); - } - } - } - - Ok(certs) -} - -/// Locate code signing certificates in the macOS keychain. -pub fn keychain_find_code_signing_certificates( - domain: KeychainDomain, - password: Option<&str>, -) -> Result, AppleCodesignError> { - let mut keychain = SecKeychain::default_for_domain(domain.into())?; - if password.is_some() { - keychain.unlock(password)?; - } - - let certs = find_certificates(&[keychain])?; - - Ok(certs - .into_iter() - .filter(|cert| !cert.captured.apple_code_signing_extensions().is_empty()) - .collect::>()) -} - -/// Find the x509 certificate chain for a certificate given search parameters. -/// -/// `domain` and `password` specify which keychain to operate on and whether -/// to attempt to unlock it via a password. -/// -/// `user_id` specifies the UID value in the certificate subject to search for. -/// You can find this in `Keychain Access` by clicking on the certificate in -/// question and looking for `User ID` under the `Subject Name` section. -pub fn macos_keychain_find_certificate_chain( - domain: KeychainDomain, - password: Option<&str>, - user_id: &str, -) -> Result, AppleCodesignError> { - let mut keychain = SecKeychain::default_for_domain(domain.into())?; - if password.is_some() { - keychain.unlock(password)?; - } - - // Find all certificates for the given keychain plus the system roots, which - // has the root CAs. - let keychains = vec![SecKeychain::open(SYSTEM_ROOTS_KEYCHAIN)?, keychain]; - - let certs = find_certificates(&keychains)?; - - // Now search for the requested start certificate and pull the thread until - // we get to a self-signed certificate. - let start_cert: &CapturedX509Certificate = certs - .iter() - .find_map(|cert| { - if let Ok(Some(value)) = cert - .captured - .subject_name() - .find_first_attribute_string(Oid(OID_USER_ID.as_ref().into())) - { - if value == user_id { - Some(&cert.captured) - } else { - None - } - } else { - None - } - }) - .ok_or_else(|| AppleCodesignError::CertificateNotFound(format!("UID={}", user_id)))?; - - let mut chain = vec![start_cert.clone()]; - let mut last_issuer_name = start_cert.issuer_name(); - - loop { - let issuer = certs.iter().find_map(|cert| { - if cert.captured.subject_name() == last_issuer_name { - Some(&cert.captured) - } else { - None - } - }); - - if let Some(issuer) = issuer { - chain.push(issuer.clone()); - - // Self signed. Stop the chain so we don't infinite loop. - if issuer.subject_name() == issuer.issuer_name() { - break; - } else { - last_issuer_name = issuer.issuer_name(); - } - } else { - // Couldn't find issuer. Stop the search. - break; - } - } - - Ok(chain) -} diff --git a/apple-codesign/src/main.rs b/apple-codesign/src/main.rs deleted file mode 100644 index 08e0f2689..000000000 --- a/apple-codesign/src/main.rs +++ /dev/null @@ -1,3156 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -#[allow(unused)] -mod app_store_connect; -#[allow(unused)] -mod apple_certificates; -#[allow(unused)] -mod bundle_signing; -#[allow(unused)] -mod certificate; -#[allow(unused)] -mod code_directory; -#[allow(unused)] -mod code_requirement; -#[allow(unused)] -mod code_resources; -#[allow(unused)] -mod cryptography; -#[allow(unused)] -mod dmg; -#[allow(unused)] -mod embedded_signature; -#[allow(unused)] -mod embedded_signature_builder; -#[allow(unused)] -mod entitlements; -mod error; -#[allow(unused)] -mod macho; -#[allow(unused)] -mod macho_signing; -#[allow(non_upper_case_globals, unused)] -#[cfg(target_os = "macos")] -mod macos; -mod notarization; -#[allow(unused)] -mod policy; -mod reader; -mod remote_signing; -mod signing; -#[allow(unused)] -mod signing_settings; -#[allow(unused)] -mod specification; -#[allow(unused)] -mod stapling; -#[allow(unused)] -mod ticket_lookup; -#[allow(unused)] -mod verify; -#[cfg(feature = "yubikey")] -#[allow(unused)] -mod yubikey; - -use { - crate::{ - app_store_connect::UnifiedApiKey, - certificate::{ - create_self_signed_code_signing_certificate, AppleCertificate, CertificateProfile, - }, - code_directory::{CodeDirectoryBlob, CodeSignatureFlags}, - code_requirement::CodeRequirements, - cryptography::{parse_pfx_data, InMemoryPrivateKey, PrivateKey}, - embedded_signature::{Blob, CodeSigningSlot, DigestType, RequirementSetBlob}, - error::AppleCodesignError, - macho::MachFile, - reader::SignatureReader, - remote_signing::{ - session_negotiation::{ - create_session_joiner, PublicKeyInitiator, SessionInitiatePeer, SessionJoinState, - SharedSecretInitiator, - }, - RemoteSignError, UnjoinedSigningClient, - }, - signing::UnifiedSigner, - signing_settings::{SettingsScope, SigningSettings}, - }, - clap::{Arg, ArgGroup, ArgMatches, Command}, - cryptographic_message_syntax::SignedData, - difference::{Changeset, Difference}, - log::{error, warn, LevelFilter}, - spki::EncodePublicKey, - std::{ - io::Write, - path::{Path, PathBuf}, - str::FromStr, - }, - x509_certificate::{CapturedX509Certificate, EcdsaCurve, KeyAlgorithm, X509CertificateBuilder}, -}; - -#[cfg(feature = "yubikey")] -use { - crate::yubikey::YubiKey, - ::yubikey::{PinPolicy, TouchPolicy}, -}; - -#[cfg(target_os = "macos")] -use crate::macos::{ - keychain_find_code_signing_certificates, macos_keychain_find_certificate_chain, KeychainDomain, -}; - -const ANALYZE_CERTIFICATE_ABOUT: &str = "\ -Analyze an X.509 certificate for Apple code signing properties. - -Given the path to a PEM encoded X.509 certificate, this command will read -the certificate and print information about it relevant to Apple code -signing. - -The output of the command can be useful to learn about X.509 certificate -extensions used by code signing certificates and to debug low-level -properties related to certificates. -"; - -const EXTRACT_ABOUT: &str = "\ -Extract code signature data from a Mach-O binary. - -Given the path to a Mach-O binary (including fat/universal) binaries, this -command will parse and print requested data to stdout. - -The --data argument controls which data to extract and how to print it. -Possible values are: - -blobs - Low-level information on the records in the embedded code signature. -cms-info - Print important information about the CMS data structure. -cms-pem - Like cms-raw except it prints PEM encoded data, which is ASCII and - safe to print to terminals. -cms-raw - Print the payload of the CMS blob. This should be well-formed BER - encoded ASN.1 data. (This will print binary to stdout.) -cms - Print the ASN.1 decoded CMS data. -code-directory-raw - Raw binary data composing the code directory data structure. -code-directory - Information on the main code directory data structure. -code-directory-serialized - Reserialize the parsed code directory, parse it again, and then print - it like `code-directory` would. -code-directory-serialized-raw - Reserialize the parsed code directory and emit its binary. Useful - for comparing round-tripping of code directory data. -linkedit-info - Information about the __LINKEDIT Mach-O segment in the binary. -linkedit-segment-raw - Complete content of the __LINKEDIT Mach-O segment as binary. -macho-load-commands - Print information about mach-o load commands in the binary. -macho-segments - Print information about mach-o segments in the binary. -macho-target - Print mach-o targeting info (platform and OS/SDK versions). -requirements-raw - Raw binary data composing the requirements blob/slot. -requirements - Parsed code requirement statement/expression. -requirements-rust - Dump the internal Rust data structures representing the requirements - expressions. -requirements-serialized - Reserialize the code requirements blob, parse it again, and then - print it like `requirements` would. -requirements-serialized-raw - Reserialize the code requirements blob and emit its binary. -signature-raw - Raw binary data composing the signature data embedded in the binary. -superblob - The SuperBlob record and high-level details of embedded Blob - records, including digests of every Blob. -"; - -const GENERATE_SELF_SIGNED_CERTIFICATE_ABOUT: &str = "\ -Generate a self-signed certificate that can be used for code signing. - -This command will generate a new key pair using the algorithm of choice -then create an X.509 certificate wrapper for it that is signed with the -just-generated private key. The created X.509 certificate has extensions -that mark it as appropriate for code signing. - -Certificates generated with this command can be useful for local testing. -However, because it is a self-signed certificate and isn't signed by a -trusted certificate authority, Apple operating systems may refuse to -load binaries signed with it. - -By default the command prints 2 PEM encoded blocks. One block is for the -X.509 public certificate. The other is for the PKCS#8 private key (which -can include the public key). - -The `--pem-filename` argument can be specified to write the generated -certificate pair to a pair of files. The destination files will have -`.crt` and `.key` appended to the value provided. - -When the certificate is written to a file, it isn't printed to stdout. -"; - -const PARSE_CODE_SIGNING_REQUIREMENT_ABOUT: &str = "\ -Parse code signing requirement data into human readable text. - -This command can be used to parse binary code signing requirement data and -print it in various formats. - -The source input format is the binary code requirement serialization. This -is the format generated by Apple's `csreq` tool via `csreq -b`. The binary -data begins with header magic `0xfade0c00`. - -The default output format is the Code Signing Requirement Language. But the -output format can be changed via the --format argument. - -Our Code Signing Requirement Language output may differ from Apple's. For -example, `and` and `or` expressions always have their sub-expressions surrounded -by parentheses (e.g. `(a) and (b)` instead of `a and b`) and strings are always -quoted. The differences, however, should not matter to the parser or result -in a different binary serialization. -"; - -const SIGN_ABOUT: &str = "\ -Adds code signatures to a signable entity. - -This command can sign the following entities: - -* A single Mach-O binary (specified by its file path) -* A bundle (specified by its directory path) -* A DMG disk image (specified by its path) -* A XAR archive (commonly a .pkg installer file) - -If the input is Mach-O binary, it can be a single or multiple/fat/universal -Mach-O binary. If a fat binary is given, each Mach-O within that binary will -be signed. - -If the input is a bundle, the bundle will be recursively signed. If the -bundle contains nested bundles or Mach-O binaries, those will be signed -automatically. - -# Settings Scope - -The following signing settings are global and apply to all signed entities: - -* --digest -* --pem-source -* --team-name -* --timestamp-url - -The following signing settings can be scoped so they only apply to certain -entities: - -* --binary-identifier -* --code-requirements-path -* --code-resources-path -* --code-signature-flags -* --entitlements-xml-path -* --info-plist-path - -Scoped settings take the form or :. If the 2nd form -is used, the string before the first colon is parsed as a \"scoping string\". -It can have the following values: - -* `main` - Applies to the main entity being signed and all nested entities. -* `@` - e.g. `@0`. Applies to a Mach-O within a fat binary at the - specified index. 0 means the first Mach-O in a fat binary. -* `@[cpu_type=` - e.g. `@[cpu_type=7]`. Applies to a Mach-O within a fat - binary targeting a numbered CPU architecture (using numeric constants - as defined by Mach-O). -* `@[cpu_type=` - e.g. `@[cpu_type=x86_64]`. Applies to a Mach-O within - a fat binary targeting a CPU architecture identified by a string. See below - for the list of recognized values. -* `` - e.g. `path/to/file`. Applies to content at a given path. This - should be the bundle-relative path to a Mach-O binary, a nested bundle, or - a Mach-O binary within a nested bundle. If a nested bundle is referenced, - settings apply to everything within that bundle. -* `@` - e.g. `path/to/file@0`. Applies to a Mach-O within a - fat binary at the given path. If the path is to a bundle, the setting applies - to all Mach-O binaries in that bundle. -* `@[cpu_type=]` e.g. `Contents/MacOS/binary@[cpu_type=7]` - or `Contents/MacOS/binary@[cpu_type=arm64]`. Applies to a Mach-O within a - fat binary targeting a CPU architecture identified by its integer constant - or string name. If the path is to a bundle, the setting applies to all - Mach-O binaries in that bundle. - -The following named CPU architectures are recognized: - -* arm -* arm64 -* arm64_32 -* x86_64 - -Signing will traverse into nested entities: - -* A fat Mach-O binary will traverse into the multiple Mach-O binaries within. -* A bundle will traverse into nested bundles. -* A bundle will traverse non-code \"resource\" files and sign their digests. -* A bundle will traverse non-main Mach-O binaries and sign them, adding their - metadata to the signed resources file. - -# Bundle Signing Overrides Settings - -When signing bundles, some settings specified on the command line will be -ignored. This is to ensure that the produced signing data is correct. The -settings ignored include (but may not be limited to): - -* --binary-identifier for the main executable. The `CFBundleIdentifier` value - from the bundle's `Info.plist` will be used instead. -* --code-resources-path. The code resources data will be computed automatically - as part of signing the bundle. -* --info-plist-path. The `Info.plist` from the bundle will be used instead. -* --digest and --extra-digest - -# Designated Code Requirements - -When using Apple issued code signing certificates, we will attempt to apply -an appropriate designated requirement automatically during signing which -matches the behavior of what `codesign` would do. We do not yet support all -signing certificates and signing targets for this, however. So you may -need to provide your own requirements. - -Designated code requirements can be specified via --code-requirements-path. - -This file MUST contain a binary/compiled code requirements expression. We do -not (yet) support parsing the human-friendly code requirements DSL. A -binary/compiled file can be produced via Apple's `csreq` tool. e.g. -`csreq -r '=' -b /output/path`. If code requirements data is -specified, it will be parsed and displayed as part of signing to ensure it -is well-formed. - -# Code Signing Key Pair - -By default, the embedded code signature will only contain digests of the -binary and other important entities (such as entitlements and resources). -This is often referred to as \"ad-hoc\" signing. - -To use a code signing key/certificate to derive a cryptographic signature, -you must specify a source certificate to use. This can be done in the following -ways: - -* The --p12-file denotes the location to a PFX formatted file. These are - often .pfx or .p12 files. A password is required to open these files. - Specify one via --p12-password or --p12-password-file or enter a password - when prompted. -* The --pem-source argument defines paths to files containing PEM encoded - certificate/key data. (e.g. files with \"===== BEGIN CERTIFICATE =====\"). -* The --source-source argument defines paths to files containiner DER - encoded certificate/key data. -* The --keychain-domain and --keychain-fingerprint arguments can be used to - load code signing certificates from macOS keychains. These arguments are - ignored on non-macOS platforms. -* The --smartcard-slot argument defines the name of a slot in a connected - smartcard device to read from. `9c` is common. -* Arguments beginning with --remote activate *remote signing mode* and can - be used to delegate cryptographic signing operations to a separate machine. - It is strongly advised to read the user documentation on remote signing - mode at https://gregoryszorc.com/docs/apple-codesign/main/. - -If you export a code signing certificate from the macOS keychain via the -`Keychain Access` application as a .p12 file, we should be able to read these -files via --p12-file. - -When using --pem-source, certificates and public keys are parsed from -`BEGIN CERTIFICATE` and `BEGIN PRIVATE KEY` sections in the files. - -The way certificate discovery works is that --p12-file is read followed by -all values to --pem-source. The seen signing keys and certificates are -collected. After collection, there must be 0 or 1 signing keys present, or -an error occurs. The first encountered public certificate is assigned -to be paired with the signing key. All remaining certificates are assumed -to constitute the CA issuing chain and will be added to the signature -data to facilitate validation. - -If you are using an Apple-issued code signing certificate, we detect this -and automatically register the Apple CA certificate chain so it is included -in the digital signature. This matches the behavior of the `codesign` tool. - -For best results, put your private key and its corresponding X.509 certificate -in a single file, either a PFX or PEM formatted file. Then add any additional -certificates constituting the signing chain in a separate PEM file. - -When using a code signing key/certificate, a Time-Stamp Protocol server URL -can be specified via --timestamp-url. By default, Apple's server is used. The -special value \"none\" can disable using a timestamp server. - -# Selecting What to Sign - -By default, this command attempts to recursively sign everything in the source -path. This applies to: - -* Bundles. If the specified bundle has nested bundles, those nested bundles - will be signed automatically. - -It is possible to exclude nested items from signing using --exclude. This -argument takes a glob expression that matches *relative paths* from the -source path. Glob expressions can be literal string compares. Or the -following special syntax is recognized: - -* `?` matches any single character. -* `*` matches any (possibly empty) sequence of characters. -* `**` matches the current directory and arbitrary subdirectories. This sequence - must form a single path component, so both **a and b** are invalid and will - result in an error. A sequence of more than two consecutive * characters is - also invalid. -* `[...]` matches any character inside the brackets. Character sequences can also - specify ranges of characters, as ordered by Unicode, so e.g. [0-9] specifies any - character between 0 and 9 inclusive. An unclosed bracket is invalid. -* `[!...]` is the negation of `[...]`, i.e. it matches any characters not in the - brackets. -* The metacharacters `?`, `*`, `[`, `]` can be matched by using brackets (e.g. - `[?]`). When a `]` occurs immediately following `[` or `[!` then it is - interpreted as being part of, rather then ending, the character set, so `]` and - `NOT ]` can be matched by `[]]` and `[!]]` respectively. The `-` character can - be specified inside a character sequence pattern by placing it at the start or - the end, e.g. `[abc-]`. - -Currently, --exclude only applies to the relative path of nested bundles within -the main bundle to sign. e.g. if you sign `MyApp.app` and it has a -`Contents/Frameworks/MyFramework.framework` that you wish to exclude, you would -`--exclude Contents/Frameworks/MyFramework.framework` or even -`--exclude Contents/Frameworks/**` to exclude the entire directory tree. - -Exclusions will still be copied and parents that need to reference exclude -entities will continue to do so. If you wish to make a file or directory -disappear, create a new directory without the file(s) and sign that. - -To exclude all nested bundles from being signed and only sign the main bundle -(the default behavior of ``codesign`` without ``--deep``), use `--exclude '**'`. -"; - -const APPLE_TIMESTAMP_URL: &str = "http://timestamp.apple.com/ts01"; - -const SUPPORTED_HASHES: &[&str; 6] = &[ - "none", - "sha1", - "sha256", - "sha256-truncated", - "sha384", - "sha512", -]; - -fn parse_scoped_value(s: &str) -> Result<(SettingsScope, &str), AppleCodesignError> { - let parts = s.splitn(2, ':').collect::>(); - - match parts.len() { - 1 => Ok((SettingsScope::Main, s)), - 2 => Ok((SettingsScope::try_from(parts[0])?, parts[1])), - _ => Err(AppleCodesignError::CliBadArgument), - } -} - -fn remote_initialization_args(own: Option<&str>) -> Vec<&'static str> { - [ - "remote_public_key", - "remote_public_key_pem_file", - "remote_shared_secret", - "remote_shared_secret_env", - ] - .into_iter() - .filter(|x| own != Some(*x)) - .collect::>() -} - -fn add_certificate_source_args(app: Command) -> Command { - app.arg( - Arg::new("smartcard_slot") - .long("smartcard-slot") - .takes_value(true) - .help("Smartcard slot number of signing certificate to use (9c is common)"), - ) - .arg( - Arg::new("keychain_domain") - .long("keychain-domain") - .takes_value(true) - .possible_values(&["user", "system", "common", "dynamic"]) - .multiple_occurrences(true) - .multiple_values(true) - .help("(macOS only) Keychain domain to operate on"), - ) - .arg( - Arg::new("keychain_fingerprint") - .long("keychain-fingerprint") - .takes_value(true) - .help("(macOS only) SHA-256 fingerprint of certificate in Keychain to use"), - ) - .arg( - Arg::new("pem_source") - .long("pem-source") - .takes_value(true) - .multiple_occurrences(true) - .multiple_values(true) - .help("Path to file containing PEM encoded certificate/key data"), - ) - .arg( - Arg::new("der_source") - .long("der-source") - .takes_value(true) - .multiple_occurrences(true) - .multiple_values(true) - .help("Path to file containing DER encoded certificate data"), - ) - .arg( - Arg::new("p12_path") - .long("p12-file") - .alias("pfx-file") - .takes_value(true) - .help("Path to a .p12/PFX file containing a certificate key pair"), - ) - .arg( - Arg::new("p12_password") - .long("p12-password") - .alias("pfx-password") - .takes_value(true) - .help("The password to use to open the --p12-file file"), - ) - .arg( - Arg::new("p12_password_file") - .long("p12-password-file") - .alias("pfx-password-file") - .conflicts_with("p12_password") - .takes_value(true) - .help("Path to file containing password for opening --p12-file file"), - ) - .arg( - Arg::new("remote_signer") - .long("remote-signer") - .requires("remote_initialization") - .help("Send signing requests to a remote server"), - ) - .arg( - Arg::new("remote_public_key") - .long("remote-public-key") - .takes_value(true) - .conflicts_with_all(&remote_initialization_args(Some("remote_public_key"))) - .help("Base64 encoded public key data describing the signer"), - ) - .arg( - Arg::new("remote_public_key_pem_file") - .long("remote-public-key-pem-file") - .takes_value(true) - .conflicts_with_all(&remote_initialization_args(Some( - "remote_public_key_pem_file", - ))) - .help("PEM encoded public key data describing the signer"), - ) - .arg( - Arg::new("remote_shared_secret") - .long("remote-shared-secret") - .conflicts_with_all(&remote_initialization_args(Some("remote_shared_secret"))) - .takes_value(true) - .help("Shared secret used for remote signing"), - ) - .arg( - Arg::new("remote_shared_secret_env") - .long("remote-shared-secret-env") - .conflicts_with_all(&remote_initialization_args(Some( - "remote_shared_secret_env", - ))) - .takes_value(true) - .help("Environment variable holding the shared secret used for remote signing"), - ) - .arg( - Arg::new("remote_signing_url") - .long("remote-signing-url") - .takes_value(true) - .default_value(crate::remote_signing::DEFAULT_SERVER_URL) - .help("URL of a remote code signing server"), - ) - .group(ArgGroup::new("keychain").args(&["keychain_domain", "keychain_fingerprint"])) - .group(ArgGroup::new("remote_initialization").args(&remote_initialization_args(None))) -} - -fn get_remote_signing_initiator( - args: &ArgMatches, -) -> Result, RemoteSignError> { - let server_url = args.value_of("remote_signing_url").map(|x| x.to_string()); - - if let Some(public_key_data) = args.value_of("remote_public_key") { - let public_key_data = base64::decode(public_key_data)?; - - Ok(Box::new(PublicKeyInitiator::new( - public_key_data, - server_url, - )?)) - } else if let Some(path) = args.value_of("remote_public_key_pem_file") { - let pem_data = std::fs::read(path)?; - let doc = pem::parse(pem_data)?; - - let spki_der = match doc.tag.as_str() { - "PUBLIC KEY" => doc.contents, - "CERTIFICATE" => { - let cert = CapturedX509Certificate::from_der(doc.contents)?; - cert.to_public_key_der()?.as_ref().to_vec() - } - tag => { - error!( - "unknown PEM format: {}; only `PUBLIC KEY` and `CERTIFICATE` are parsed", - tag - ); - return Err(RemoteSignError::Crypto("invalid public key data".into())); - } - }; - - Ok(Box::new(PublicKeyInitiator::new(spki_der, server_url)?)) - } else if let Some(env) = args.value_of("remote_shared_secret_env") { - let secret = std::env::var(env).map_err(|_| { - RemoteSignError::ClientState("failed reading from shared secret environment variable") - })?; - - Ok(Box::new(SharedSecretInitiator::new( - secret.as_bytes().to_vec(), - )?)) - } else if let Some(value) = args.value_of("remote_shared_secret") { - Ok(Box::new(SharedSecretInitiator::new( - value.as_bytes().to_vec(), - )?)) - } else { - error!("no arguments provided to establish session with remote signer"); - error!( - "specify --remote-public-key, --remote-shared-secret-env, or --remote-shared-secret" - ); - Err(RemoteSignError::ClientState( - "unable to initiate remote signing", - )) - } -} - -fn collect_certificates_from_args( - args: &ArgMatches, - scan_smartcard: bool, -) -> Result<(Vec>, Vec), AppleCodesignError> { - let mut keys: Vec> = vec![]; - let mut certs = vec![]; - - if let Some(p12_path) = args.value_of("p12_path") { - let p12_data = std::fs::read(p12_path)?; - - let p12_password = if let Some(password) = args.value_of("p12_password") { - password.to_string() - } else if let Some(path) = args.value_of("p12_password_file") { - std::fs::read_to_string(path)? - .lines() - .next() - .expect("should get a single line") - .to_string() - } else { - dialoguer::Password::new() - .with_prompt("Please enter password for p12 file") - .interact()? - }; - - let (cert, key) = parse_pfx_data(&p12_data, &p12_password)?; - - keys.push(Box::new(key)); - certs.push(cert); - } - - if let Some(values) = args.values_of("pem_source") { - for pem_source in values { - warn!("reading PEM data from {}", pem_source); - let pem_data = std::fs::read(pem_source)?; - - for pem in pem::parse_many(&pem_data).map_err(AppleCodesignError::CertificatePem)? { - match pem.tag.as_str() { - "CERTIFICATE" => { - certs.push(CapturedX509Certificate::from_der(pem.contents)?); - } - "PRIVATE KEY" => { - keys.push(Box::new(InMemoryPrivateKey::from_pkcs8_der(&pem.contents)?)) - } - tag => warn!("(unhandled PEM tag {}; ignoring)", tag), - } - } - } - } - - if let Some(values) = args.values_of("der_source") { - for der_source in values { - warn!("reading DER file {}", der_source); - let der_data = std::fs::read(der_source)?; - - certs.push(CapturedX509Certificate::from_der(der_data)?); - } - } - - find_certificates_in_keychain(args, &mut keys, &mut certs)?; - - if scan_smartcard { - if let Some(slot) = args.value_of("smartcard_slot") { - handle_smartcard_sign_slot(slot, &mut keys, &mut certs)?; - } - } - - let remote_signing_url = if args.is_present("remote_signer") { - args.value_of("remote_signing_url") - } else { - None - }; - - if let Some(remote_signing_url) = remote_signing_url { - let initiator = get_remote_signing_initiator(args)?; - - let client = UnjoinedSigningClient::new_initiator( - remote_signing_url, - initiator, - Some(print_session_join), - )?; - - // As part of the handshake we obtained the public certificates from the signer. - // So make them the canonical set. - if !certs.is_empty() { - warn!( - "ignoring {} local certificates and using remote signer's certificate(s)", - certs.len() - ); - } - - certs = vec![client.signing_certificate().clone()]; - certs.extend(client.certificate_chain().iter().cloned()); - - // The client implements Sign, so we just use it as the private key. - keys = vec![Box::new(client)]; - } - - Ok((keys, certs)) -} - -/// Add arguments common to commands that interact with the Notary API. -fn add_notary_api_args(app: Command) -> Command { - app.arg( - Arg::new("api_key_path") - .long("api-key-path") - .takes_value(true) - .allow_invalid_utf8(true) - .conflicts_with_all(&["api_issuer", "api_key"]) - .help("Path to a JSON file containing the API Key"), - ) - .arg( - Arg::new("api_issuer") - .long("api-issuer") - .takes_value(true) - .requires("api_key") - .help("App Store Connect Issuer ID (likely a UUID)"), - ) - .arg( - Arg::new("api_key") - .long("api-key") - .takes_value(true) - .requires("api_issuer") - .help("App Store Connect API Key ID"), - ) -} - -fn add_yubikey_policy_args(app: Command) -> Command { - app.arg( - Arg::new("touch_policy") - .long("touch-policy") - .takes_value(true) - .possible_values(["default", "always", "never", "cached"]) - .default_value("default") - .help("Smartcard touch policy to protect key access"), - ) - .arg( - Arg::new("pin_policy") - .long("pin-policy") - .takes_value(true) - .possible_values(["default", "never", "once", "always"]) - .default_value("default") - .help("Smartcard pin prompt policy to protect key access"), - ) -} - -#[cfg(feature = "yubikey")] -fn str_to_touch_policy(s: &str) -> Result { - match s { - "default" => Ok(TouchPolicy::Default), - "never" => Ok(TouchPolicy::Never), - "always" => Ok(TouchPolicy::Always), - "cached" => Ok(TouchPolicy::Cached), - _ => Err(AppleCodesignError::CliBadArgument), - } -} - -#[cfg(feature = "yubikey")] -fn str_to_pin_policy(s: &str) -> Result { - match s { - "default" => Ok(PinPolicy::Default), - "never" => Ok(PinPolicy::Never), - "once" => Ok(PinPolicy::Once), - "always" => Ok(PinPolicy::Always), - _ => Err(AppleCodesignError::CliBadArgument), - } -} - -fn print_certificate_info(cert: &CapturedX509Certificate) -> Result<(), AppleCodesignError> { - println!( - "Subject CN: {}", - cert.subject_common_name() - .unwrap_or_else(|| "".to_string()) - ); - println!( - "Issuer CN: {}", - cert.issuer_common_name() - .unwrap_or_else(|| "".to_string()) - ); - println!("Subject is Issuer?: {}", cert.subject_is_issuer()); - println!( - "Team ID: {}", - cert.apple_team_id() - .unwrap_or_else(|| "".to_string()) - ); - println!( - "SHA-1 fingerprint: {}", - hex::encode(cert.sha1_fingerprint()?) - ); - println!( - "SHA-256 fingerprint: {}", - hex::encode(cert.sha256_fingerprint()?) - ); - if let Some(alg) = cert.key_algorithm() { - println!("Key Algorithm: {}", alg); - } - if let Some(alg) = cert.signature_algorithm() { - println!("Signature Algorithm: {}", alg); - } - println!( - "Public Key Data: {}", - base64::encode( - cert.to_public_key_der() - .map_err(|e| AppleCodesignError::X509Parse(format!( - "error constructing SPKI: {}", - e - )))? - ) - ); - println!( - "Signed by Apple?: {}", - cert.chains_to_apple_root_ca() - ); - if cert.chains_to_apple_root_ca() { - println!("Apple Issuing Chain:"); - for signer in cert.apple_issuing_chain() { - println!( - " - {}", - signer - .subject_common_name() - .unwrap_or_else(|| "".to_string()) - ); - } - } - - println!( - "Guessed Certificate Profile: {}", - if let Some(profile) = cert.apple_guess_profile() { - format!("{:?}", profile) - } else { - "none".to_string() - } - ); - println!("Is Apple Root CA?: {}", cert.is_apple_root_ca()); - println!( - "Is Apple Intermediate CA?: {}", - cert.is_apple_intermediate_ca() - ); - println!( - "Apple CA Extension: {}", - if let Some(ext) = cert.apple_ca_extension() { - format!("{} ({:?})", ext.as_oid(), ext) - } else { - "none".to_string() - } - ); - println!("Apple Extended Key Usage Purpose Extensions:"); - for purpose in cert.apple_extended_key_usage_purposes() { - println!(" - {} ({:?})", purpose.as_oid(), purpose); - } - println!("Apple Code Signing Extensions:"); - for ext in cert.apple_code_signing_extensions() { - println!(" - {} ({:?})", ext.as_oid(), ext); - } - print!( - "\n{}", - cert.to_public_key_pem(Default::default()) - .map_err(|e| AppleCodesignError::X509Parse(format!( - "error constructing SPKI: {}", - e - )))? - ); - print!("\n{}", cert.encode_pem()); - - Ok(()) -} - -fn print_session_join(sjs_base64: &str, sjs_pem: &str) -> Result<(), RemoteSignError> { - error!(""); - error!("Run the following command to join this signing session:"); - error!(""); - error!(" rcodesign remote-sign {}", sjs_base64); - error!(""); - error!("Or if this output is too long, paste the following output:"); - error!(""); - for line in sjs_pem.lines() { - error!("{}", line); - } - error!(""); - error!("Into an interactive editor using:"); - error!(""); - error!(" rcodesign remote-sign --editor"); - error!(""); - error!("Or into a new file whose path you define with:"); - error!(""); - error!(" rcodesign remote-sign --sjs-path /path/to/file/you/just/saved"); - error!(""); - error!("(waiting for remote signer to join)"); - - Ok(()) -} - -#[allow(unused)] -fn prompt_smartcard_pin() -> Result, AppleCodesignError> { - let pin = dialoguer::Password::new() - .with_prompt("Please enter device PIN") - .interact()?; - - Ok(pin.as_bytes().to_vec()) -} - -#[cfg(feature = "yubikey")] -fn handle_smartcard_sign_slot( - slot: &str, - private_keys: &mut Vec>, - public_certificates: &mut Vec, -) -> Result<(), AppleCodesignError> { - let slot_id = ::yubikey::piv::SlotId::from_str(slot)?; - let formatted = hex::encode([u8::from(slot_id)]); - let mut yk = YubiKey::new()?; - yk.set_pin_callback(prompt_smartcard_pin); - - if let Some(cert) = yk.get_certificate_signer(slot_id)? { - warn!("using certificate in smartcard slot {}", formatted); - public_certificates.push(cert.certificate().clone()); - private_keys.push(Box::new(cert)); - - Ok(()) - } else { - Err(AppleCodesignError::SmartcardNoCertificate(formatted)) - } -} - -#[cfg(not(feature = "yubikey"))] -fn handle_smartcard_sign_slot( - _slot: &str, - _private_keys: &mut [Box], - _public_certificates: &mut [CapturedX509Certificate], -) -> Result<(), AppleCodesignError> { - error!("smartcard support not available; ignoring --smartcard-slot"); - - Ok(()) -} - -#[cfg(target_os = "macos")] -fn find_certificates_in_keychain( - args: &ArgMatches, - private_keys: &mut Vec>, - public_certificates: &mut Vec, -) -> Result<(), AppleCodesignError> { - // No arguments pertinent to keychains. Don't even speak to the - // keychain API since this could only error. - if args.occurrences_of("keychain") == 0 { - return Ok(()); - } - - // Collect all the keychain domains to search. - let domains = if let Some(domains) = args.values_of("keychain_domain") { - domains - .into_iter() - .map(|x| x.to_string()) - .collect::>() - } else { - vec!["user".to_string()] - }; - - let domains = domains - .into_iter() - .map(|domain| { - KeychainDomain::try_from(domain.as_str()) - .expect("clap should have validated domain values") - }) - .collect::>(); - - // Now iterate all the keychains and try to find requested certificates. - - for domain in domains { - for cert in keychain_find_code_signing_certificates(domain, None)? { - let matches = if let Some(wanted_fingerprint) = args.value_of("keychain_fingerprint") { - let got_fingerprint = hex::encode(cert.sha256_fingerprint()?.as_ref()); - - wanted_fingerprint.to_ascii_lowercase() == got_fingerprint.to_ascii_lowercase() - } else { - false - }; - - if matches { - public_certificates.push(cert.as_captured_x509_certificate()); - private_keys.push(Box::new(cert)); - } - } - } - - Ok(()) -} - -#[cfg(not(target_os = "macos"))] -fn find_certificates_in_keychain( - args: &ArgMatches, - _private_keys: &mut [Box], - _public_certificates: &mut [CapturedX509Certificate], -) -> Result<(), AppleCodesignError> { - if args.occurrences_of("keychain") > 0 { - error!( - "--keychain* arguments only supported on macOS and will be ignored on this platform" - ); - } - - Ok(()) -} - -fn command_analyze_certificate(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let certs = collect_certificates_from_args(args, true)?.1; - - for (i, cert) in certs.into_iter().enumerate() { - println!("# Certificate {}", i); - println!(); - print_certificate_info(&cert)?; - println!(); - } - - Ok(()) -} - -fn command_compute_code_hashes(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let path = args - .value_of("path") - .ok_or(AppleCodesignError::CliBadArgument)?; - let index = args.value_of("universal_index").unwrap(); - let index = usize::from_str(index).map_err(|_| AppleCodesignError::CliBadArgument)?; - let hash_type = DigestType::try_from(args.value_of("hash").unwrap())?; - let page_size = usize::from_str( - args.value_of("page_size") - .expect("page_size should have default value"), - ) - .map_err(|_| AppleCodesignError::CliBadArgument)?; - - let data = std::fs::read(path)?; - let mach = MachFile::parse(&data)?; - let macho = mach.nth_macho(index)?; - - let hashes = macho.code_digests(hash_type, page_size)?; - - for hash in hashes { - println!("{}", hex::encode(hash)); - } - - Ok(()) -} - -fn command_diff_signatures(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let path0 = args - .value_of("path0") - .ok_or(AppleCodesignError::CliBadArgument)?; - let path1 = args - .value_of("path1") - .ok_or(AppleCodesignError::CliBadArgument)?; - - let reader = SignatureReader::from_path(path0)?; - - let a_entities = reader.entities()?; - - let reader = SignatureReader::from_path(path1)?; - let b_entities = reader.entities()?; - - let a = serde_yaml::to_string(&a_entities)?; - let b = serde_yaml::to_string(&b_entities)?; - - let Changeset { diffs, .. } = Changeset::new(&a, &b, "\n"); - - for item in diffs { - match item { - Difference::Same(ref x) => { - for line in x.lines() { - println!(" {}", line); - } - } - Difference::Add(ref x) => { - for line in x.lines() { - println!("+{}", line); - } - } - Difference::Rem(ref x) => { - for line in x.lines() { - println!("-{}", line); - } - } - } - } - - Ok(()) -} - -const ENCODE_APP_STORE_CONNECT_API_KEY_ABOUT: &str = "\ -Encode an App Store Connect API Key to JSON. - -App Store Connect API Keys -(https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api) -are defined by 3 components: - -* The Issuer ID (likely a UUID) -* A Key ID (an alphanumeric value like `DEADBEEF42`) -* A PEM encoded ECDSA private key (typically a file beginning with - `-----BEGIN PRIVATE KEY-----`). - -This command is used to encode all API Key components into a single JSON -object so you only have to refer to a single entity when performing -operations (like notarization) using these API Keys. - -The API Key components are specified as positional arguments. - -By default, the JSON encoded unified representation is printed to stdout. -You can write to a file instead by passing `--output-path `. - -# Security Considerations - -The App Store Connect API Key contains a private key and its value should be -treated as sensitive: if an unwanted party obtains your private key, they -effectively have access to your App Store Connect account. - -When this command writes JSON files, an attempt is made to limit access -to the file. However, file access restrictions may not be as secure as you -want. Security conscious individuals should audit the permissions of the -file and adjust accordingly. -"; - -fn command_encode_app_store_connect_api_key(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let issuer_id = args - .value_of("issuer_id") - .expect("arg should have been required"); - let key_id = args - .value_of("key_id") - .expect("arg should have been required"); - let private_key_path = Path::new( - args.value_of_os("private_key_path") - .expect("arg should have been required"), - ); - - let unified = UnifiedApiKey::from_ecdsa_pem_path(issuer_id, key_id, private_key_path)?; - - if let Some(output_path) = args.value_of_os("output_path") { - let output_path = Path::new(output_path); - - eprintln!("writing unified key JSON to {}", output_path.display()); - unified.write_json_file(output_path)?; - eprintln!( - "consider auditing the file's access permissions to ensure its content remains secure" - ); - } else { - println!("{}", unified.to_json_string()?); - } - - Ok(()) -} - -fn print_signed_data( - prefix: &str, - signed_data: &SignedData, - external_content: Option>, -) -> Result<(), AppleCodesignError> { - println!( - "{}signed content (embedded): {:?}", - prefix, - signed_data.signed_content().map(hex::encode) - ); - println!( - "{}signed content (external): {:?}... ({} bytes)", - prefix, - external_content.as_ref().map(|x| hex::encode(&x[0..40])), - external_content.as_ref().map(|x| x.len()).unwrap_or(0), - ); - - let content = if let Some(v) = signed_data.signed_content() { - Some(v) - } else { - external_content.as_ref().map(|v| v.as_ref()) - }; - - if let Some(content) = content { - println!( - "{}signed content SHA-1: {}", - prefix, - hex::encode(DigestType::Sha1.digest_data(content)?) - ); - println!( - "{}signed content SHA-256: {}", - prefix, - hex::encode(DigestType::Sha256.digest_data(content)?) - ); - println!( - "{}signed content SHA-384: {}", - prefix, - hex::encode(DigestType::Sha384.digest_data(content)?) - ); - println!( - "{}signed content SHA-512: {}", - prefix, - hex::encode(DigestType::Sha512.digest_data(content)?) - ); - } - println!( - "{}certificate count: {}", - prefix, - signed_data.certificates().count() - ); - for (i, cert) in signed_data.certificates().enumerate() { - println!( - "{}certificate #{}: subject CN={}; self signed={}", - prefix, - i, - cert.subject_common_name() - .unwrap_or_else(|| "".to_string()), - cert.subject_is_issuer() - ); - } - println!("{}signer count: {}", prefix, signed_data.signers().count()); - for (i, signer) in signed_data.signers().enumerate() { - println!( - "{}signer #{}: digest algorithm: {:?}", - prefix, - i, - signer.digest_algorithm() - ); - println!( - "{}signer #{}: signature algorithm: {:?}", - prefix, - i, - signer.signature_algorithm() - ); - - if let Some(sa) = signer.signed_attributes() { - println!( - "{}signer #{}: content type: {}", - prefix, - i, - sa.content_type() - ); - println!( - "{}signer #{}: message digest: {}", - prefix, - i, - hex::encode(sa.message_digest()) - ); - println!( - "{}signer #{}: signing time: {:?}", - prefix, - i, - sa.signing_time() - ); - } - - let digested_data = signer.signed_content_with_signed_data(signed_data); - - println!( - "{}signer #{}: signature content SHA-1: {}", - prefix, - i, - hex::encode(DigestType::Sha1.digest_data(&digested_data)?) - ); - println!( - "{}signer #{}: signature content SHA-256: {}", - prefix, - i, - hex::encode(DigestType::Sha256.digest_data(&digested_data)?) - ); - println!( - "{}signer #{}: signature content SHA-384: {}", - prefix, - i, - hex::encode(DigestType::Sha384.digest_data(&digested_data)?) - ); - println!( - "{}signer #{}: signature content SHA-512: {}", - prefix, - i, - hex::encode(DigestType::Sha512.digest_data(&digested_data)?) - ); - - if signed_data.signed_content().is_some() { - println!( - "{}signer #{}: digest valid: {}", - prefix, - i, - signer - .verify_message_digest_with_signed_data(signed_data) - .is_ok() - ); - } - println!( - "{}signer #{}: signature valid: {}", - prefix, - i, - signer - .verify_signature_with_signed_data(signed_data) - .is_ok() - ); - - println!( - "{}signer #{}: time-stamp token present: {}", - prefix, - i, - signer.time_stamp_token_signed_data()?.is_some() - ); - - if let Some(tsp_signed_data) = signer.time_stamp_token_signed_data()? { - let prefix = format!("{}signer #{}: time-stamp token: ", prefix, i); - - print_signed_data(&prefix, &tsp_signed_data, None)?; - } - } - - Ok(()) -} - -fn command_extract(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let path = args - .value_of("path") - .ok_or(AppleCodesignError::CliBadArgument)?; - let format = args - .value_of("data") - .ok_or(AppleCodesignError::CliBadArgument)?; - let index = args.value_of("universal_index").unwrap(); - let index = usize::from_str(index).map_err(|_| AppleCodesignError::CliBadArgument)?; - - let data = std::fs::read(path)?; - let mach = MachFile::parse(&data)?; - let macho = mach.nth_macho(index)?; - - match format { - "blobs" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - for blob in embedded.blobs { - let parsed = blob.into_parsed_blob()?; - println!("{:#?}", parsed); - } - } - "cms-info" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Some(cms) = embedded.signature_data()? { - let signed_data = SignedData::parse_ber(cms)?; - - let cd_data = if let Ok(Some(blob)) = embedded.code_directory() { - Some(blob.to_blob_bytes()?) - } else { - None - }; - - print_signed_data("", &signed_data, cd_data)?; - } else { - eprintln!("no CMS data"); - } - } - "cms-pem" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Some(cms) = embedded.signature_data()? { - print!( - "{}", - pem::encode(&pem::Pem { - tag: "PKCS7".to_string(), - contents: cms.to_vec(), - }) - ); - } else { - eprintln!("no CMS data"); - } - } - "cms-raw" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Some(cms) = embedded.signature_data()? { - std::io::stdout().write_all(cms)?; - } else { - eprintln!("no CMS data"); - } - } - "cms" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Some(signed_data) = embedded.signed_data()? { - println!("{:#?}", signed_data); - } else { - eprintln!("no CMS data"); - } - } - "code-directory-raw" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Some(blob) = embedded.find_slot(CodeSigningSlot::CodeDirectory) { - std::io::stdout().write_all(blob.data)?; - } else { - eprintln!("no code directory"); - } - } - "code-directory-serialized-raw" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Ok(Some(cd)) = embedded.code_directory() { - std::io::stdout().write_all(&cd.to_blob_bytes()?)?; - } else { - eprintln!("no code directory"); - } - } - "code-directory-serialized" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Ok(Some(cd)) = embedded.code_directory() { - let serialized = cd.to_blob_bytes()?; - println!("{:#?}", CodeDirectoryBlob::from_blob_bytes(&serialized)?); - } - } - "code-directory" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Some(cd) = embedded.code_directory()? { - println!("{:#?}", cd); - } else { - eprintln!("no code directory"); - } - } - "linkedit-info" => { - let sig = macho - .find_signature_data()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - println!("__LINKEDIT segment index: {}", sig.linkedit_segment_index); - println!( - "__LINKEDIT segment start offset: {}", - sig.linkedit_segment_start_offset - ); - println!( - "__LINKEDIT segment end offset: {}", - sig.linkedit_segment_end_offset - ); - println!( - "__LINKEDIT segment size: {}", - sig.linkedit_segment_data.len() - ); - println!( - "__LINKEDIT signature global start offset: {}", - sig.linkedit_signature_start_offset - ); - println!( - "__LINKEDIT signature global end offset: {}", - sig.linkedit_signature_end_offset - ); - println!( - "__LINKEDIT signature local segment start offset: {}", - sig.signature_start_offset - ); - println!( - "__LINKEDIT signature local segment end offset: {}", - sig.signature_end_offset - ); - println!("__LINKEDIT signature size: {}", sig.signature_data.len()); - } - "linkedit-segment-raw" => { - let sig = macho - .find_signature_data()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - std::io::stdout().write_all(sig.linkedit_segment_data)?; - } - "macho-load-commands" => { - println!("load command count: {}", macho.macho.load_commands.len()); - - for command in &macho.macho.load_commands { - println!( - "{}; offsets=0x{:x}-0x{:x} ({}-{}); size={}", - goblin::mach::load_command::cmd_to_str(command.command.cmd()), - command.offset, - command.offset + command.command.cmdsize(), - command.offset, - command.offset + command.command.cmdsize(), - command.command.cmdsize(), - ); - } - } - "macho-segments" => { - println!("segments count: {}", macho.macho.segments.len()); - for (segment_index, segment) in macho.macho.segments.iter().enumerate() { - let sections = segment.sections()?; - - println!( - "segment #{}; {}; offsets=0x{:x}-0x{:x}; vm/file size {}/{}; section count {}", - segment_index, - segment.name()?, - segment.fileoff, - segment.fileoff as usize + segment.data.len(), - segment.vmsize, - segment.filesize, - sections.len() - ); - for (section_index, (section, _)) in sections.into_iter().enumerate() { - println!( - "segment #{}; section #{}: {}; segment offsets=0x{:x}-0x{:x} size {}", - segment_index, - section_index, - section.name()?, - section.offset, - section.offset as u64 + section.size, - section.size - ); - } - } - } - "macho-target" => { - if let Some(target) = macho.find_targeting()? { - println!("Platform: {}", target.platform); - println!("Minimum OS: {}", target.minimum_os_version); - println!("SDK: {}", target.sdk_version); - } else { - println!("Unable to resolve Mach-O targeting from load commands"); - } - } - "requirements-raw" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Some(blob) = embedded.find_slot(CodeSigningSlot::RequirementSet) { - std::io::stdout().write_all(blob.data)?; - } else { - eprintln!("no requirements"); - } - } - "requirements-rust" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Some(reqs) = embedded.code_requirements()? { - for (typ, req) in &reqs.requirements { - for expr in req.parse_expressions()?.iter() { - println!("{} => {:#?}", typ, expr); - } - } - } else { - eprintln!("no requirements"); - } - } - "requirements-serialized-raw" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Some(reqs) = embedded.code_requirements()? { - std::io::stdout().write_all(&reqs.to_blob_bytes()?)?; - } else { - eprintln!("no requirements"); - } - } - "requirements-serialized" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Some(reqs) = embedded.code_requirements()? { - let serialized = reqs.to_blob_bytes()?; - println!("{:#?}", RequirementSetBlob::from_blob_bytes(&serialized)?); - } else { - eprintln!("no requirements"); - } - } - "requirements" => { - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - if let Some(reqs) = embedded.code_requirements()? { - for (typ, req) in &reqs.requirements { - for expr in req.parse_expressions()?.iter() { - println!("{} => {}", typ, expr); - } - } - } else { - eprintln!("no requirements"); - } - } - "signature-raw" => { - let sig = macho - .find_signature_data()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - std::io::stdout().write_all(sig.signature_data)?; - } - "superblob" => { - let sig = macho - .find_signature_data()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - let embedded = macho - .code_signature()? - .ok_or(AppleCodesignError::BinaryNoCodeSignature)?; - - println!("file start offset: {}", sig.linkedit_signature_start_offset); - println!("file end offset: {}", sig.linkedit_signature_end_offset); - println!("__LINKEDIT start offset: {}", sig.signature_start_offset); - println!("__LINKEDIT end offset: {}", sig.signature_end_offset); - println!("length: {}", embedded.length); - println!("blob count: {}", embedded.count); - println!("blobs:"); - for blob in embedded.blobs { - println!("- index: {}", blob.index); - println!( - " offsets: 0x{:x}-0x{:x} ({}-{})", - blob.offset, - blob.offset + blob.length - 1, - blob.offset, - blob.offset + blob.length - 1 - ); - println!(" length: {}", blob.length); - println!(" slot: {:?}", blob.slot); - println!(" magic: {:?} (0x{:x})", blob.magic, u32::from(blob.magic)); - println!( - " sha1: {}", - hex::encode(blob.digest_with(DigestType::Sha1)?) - ); - println!( - " sha256: {}", - hex::encode(blob.digest_with(DigestType::Sha256)?) - ); - println!( - " sha256-truncated: {}", - hex::encode(blob.digest_with(DigestType::Sha256Truncated)?) - ); - println!( - " sha384: {}", - hex::encode(blob.digest_with(DigestType::Sha384)?), - ); - println!( - " sha512: {}", - hex::encode(blob.digest_with(DigestType::Sha512)?), - ); - println!( - " sha1-base64: {}", - base64::encode(blob.digest_with(DigestType::Sha1)?) - ); - println!( - " sha256-base64: {}", - base64::encode(blob.digest_with(DigestType::Sha256)?) - ); - println!( - " sha256-truncated-base64: {}", - base64::encode(blob.digest_with(DigestType::Sha256Truncated)?) - ); - println!( - " sha384-base64: {}", - base64::encode(blob.digest_with(DigestType::Sha384)?) - ); - println!( - " sha512-base64: {}", - base64::encode(blob.digest_with(DigestType::Sha512)?) - ); - } - } - _ => panic!("unhandled format: {}", format), - } - - Ok(()) -} - -fn command_generate_certificate_signing_request( - args: &ArgMatches, -) -> Result<(), AppleCodesignError> { - let csr_pem_path = args.value_of("csr_pem_path").map(PathBuf::from); - - let (private_keys, _) = collect_certificates_from_args(args, true)?; - - let private_key = if private_keys.is_empty() { - error!("no private keys found; a private key is required to sign a certificate signing request"); - return Err(AppleCodesignError::CliBadArgument); - } else if private_keys.len() > 1 { - error!( - "at most 1 private key can be present (found {}); aborting", - private_keys.len() - ); - return Err(AppleCodesignError::CliBadArgument); - } else { - private_keys.into_iter().next().expect("checked size above") - }; - - let key_algorithm = private_key.key_algorithm().ok_or_else(|| { - error!("unable to determine key algorithm of private key (please report this issue)"); - AppleCodesignError::CliBadArgument - })?; - - let mut builder = X509CertificateBuilder::new(key_algorithm); - builder - .subject() - .append_common_name_utf8_string("Apple Code Signing CSR") - .map_err(|e| AppleCodesignError::CertificateBuildError(format!("{:?}", e)))?; - - warn!("generating CSR; you may be prompted to enter credentials to unlock the signing key"); - let pem = builder - .create_certificate_signing_request(private_key.as_key_info_signer())? - .encode_pem()?; - - if let Some(dest_path) = csr_pem_path { - if let Some(parent) = dest_path.parent() { - std::fs::create_dir_all(parent)?; - } - - warn!("writing PEM encoded CSR to {}", dest_path.display()); - std::fs::write(&dest_path, pem.as_bytes())?; - } - - print!("{}", pem); - - Ok(()) -} - -fn command_generate_self_signed_certificate(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let algorithm = match args - .value_of("algorithm") - .ok_or(AppleCodesignError::CliBadArgument)? - { - "ecdsa" => KeyAlgorithm::Ecdsa(EcdsaCurve::Secp256r1), - "ed25519" => KeyAlgorithm::Ed25519, - value => panic!( - "algorithm values should have been validated by arg parser: {}", - value - ), - }; - - let profile = args - .value_of("profile") - .ok_or(AppleCodesignError::CliBadArgument)?; - let profile = CertificateProfile::from_str(profile)?; - let team_id = args - .value_of("team_id") - .ok_or(AppleCodesignError::CliBadArgument)?; - let person_name = args - .value_of("person_name") - .ok_or(AppleCodesignError::CliBadArgument)?; - let country_name = args - .value_of("country_name") - .ok_or(AppleCodesignError::CliBadArgument)?; - - let validity_days = args.value_of("validity_days").unwrap(); - let validity_days = - i64::from_str(validity_days).map_err(|_| AppleCodesignError::CliBadArgument)?; - - let pem_filename = args.value_of("pem_filename"); - - let validity_duration = chrono::Duration::days(validity_days); - - let (cert, _, raw) = create_self_signed_code_signing_certificate( - algorithm, - profile, - team_id, - person_name, - country_name, - validity_duration, - )?; - - let cert_pem = cert.encode_pem(); - let key_pem = pem::encode(&pem::Pem { - tag: "PRIVATE KEY".to_string(), - contents: raw.as_ref().to_vec(), - }); - - let mut wrote_file = false; - - if let Some(pem_filename) = pem_filename { - let cert_path = PathBuf::from(format!("{}.crt", pem_filename)); - let key_path = PathBuf::from(format!("{}.key", pem_filename)); - - if let Some(parent) = cert_path.parent() { - std::fs::create_dir_all(parent)?; - } - - println!("writing public certificate to {}", cert_path.display()); - std::fs::write(&cert_path, cert_pem.as_bytes())?; - println!("writing private signing key to {}", key_path.display()); - std::fs::write(&key_path, key_pem.as_bytes())?; - - wrote_file = true; - } - - if !wrote_file { - print!("{}", cert_pem); - print!("{}", key_pem); - } - - Ok(()) -} - -#[cfg(target_os = "macos")] -fn command_keychain_export_certificate_chain(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let user_id = args.value_of("user_id").unwrap(); - - let domain = args - .value_of("domain") - .expect("clap should have added default value"); - - let domain = - KeychainDomain::try_from(domain).expect("clap should have validated domain values"); - - let password = if let Some(path) = args.value_of("password_file") { - let data = std::fs::read_to_string(path)?; - - Some( - data.lines() - .next() - .expect("should get a single line") - .to_string(), - ) - } else if let Some(password) = args.value_of("password") { - Some(password.to_string()) - } else { - None - }; - - let certs = macos_keychain_find_certificate_chain(domain, password.as_deref(), user_id)?; - - for (i, cert) in certs.iter().enumerate() { - if args.is_present("no_print_self") && i == 0 { - continue; - } - - print!("{}", cert.encode_pem()); - } - - Ok(()) -} - -#[cfg(not(target_os = "macos"))] -fn command_keychain_export_certificate_chain(_args: &ArgMatches) -> Result<(), AppleCodesignError> { - Err(AppleCodesignError::CliGeneralError( - "macOS Keychain export only supported on macOS".to_string(), - )) -} - -#[cfg(target_os = "macos")] -fn command_keychain_print_certificates(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let domain = args - .value_of("domain") - .expect("clap should have added default value"); - - let domain = - KeychainDomain::try_from(domain).expect("clap should have validated domain values"); - - let certs = keychain_find_code_signing_certificates(domain, None)?; - - for (i, cert) in certs.into_iter().enumerate() { - println!("# Certificate {}", i); - println!(); - print_certificate_info(&cert)?; - println!(); - } - - Ok(()) -} - -#[cfg(not(target_os = "macos"))] -fn command_keychain_print_certificates(_args: &ArgMatches) -> Result<(), AppleCodesignError> { - Err(AppleCodesignError::CliGeneralError( - "macOS Keychain integration supported on macOS".to_string(), - )) -} - -const NOTARIZE_ABOUT: &str = "\ -Submit a notarization request to Apple. - -This command is used to submit an asset to Apple for notarization. Given -a path to an asset with a code signature, this command will connect to Apple's -Notary API and upload the asset. It will then optionally wait on the submission -to finish processing (which typically takes a few dozen seconds). If the -asset validates Apple's requirements, Apple will issue a *notarization ticket* -as proof that they approved of it. This ticket is then added to the asset in a -process called *stapling*, which this command can do automatically if the -`--staple` argument is passed. - -# App Store Connect API Key - -In order to communicate with Apple's servers, you need an App Store Connect -API Key. This requires an Apple Developer account. You can generate an -API Key at https://appstoreconnect.apple.com/access/api. - -The recommended mechanism to define the API Key is via `--api-key-path`, -which takes the path to a file containing JSON produced by the -`encode-app-store-connect-api-key` command. See that command's help for -more details. - -If you don't wish to use `--api-key-path`, you can define the key components -via the `--api-issuer` and `--api-key` arguments. You will need a file named -`AuthKey_.p8` in one of the following locations: `$(pwd)/private_keys/`, -`~/private_keys/`, '~/.private_keys/`, and `~/.appstoreconnect/private_keys/` -(searched in that order). The name of the file is derived from the value of -`--api-key`. - -In all cases, App Store Connect API Keys can be managed at -https://appstoreconnect.apple.com/access/api. - -# Modes of Operation - -By default, the `notarize` command will initiate an upload to Apple and exit -once the upload is complete. - -Once an upload is performed, Apple will asynchronously process the uploaded -content. This can take seconds to minutes. - -To poll Apple's servers and wait on the server-side processing to finish, -specify `--wait`. This will query the state of the processing every few seconds -until it is finished, the max wait time is reached, or an error occurs. - -To automatically staple an asset after server-side processing has finished, -specify `--staple`. This implies `--wait`. -"; - -/// Obtain a notarization client from arguments. -fn notarizer_from_args(args: &ArgMatches) -> Result { - let api_key_path = args.value_of_os("api_key_path").map(Path::new); - let api_issuer = args.value_of("api_issuer"); - let api_key = args.value_of("api_key"); - - let mut notarizer = crate::notarization::Notarizer::new()?; - - if let Some(api_key_path) = api_key_path { - let unified = UnifiedApiKey::from_json_path(api_key_path)?; - notarizer.set_token_encoder(unified.try_into()?); - } else if let (Some(issuer), Some(key)) = (api_issuer, api_key) { - notarizer.set_api_key(issuer, key)?; - } - - Ok(notarizer) -} - -fn notarizer_wait_duration(args: &ArgMatches) -> Result { - let max_wait_seconds = args - .value_of("max_wait_seconds") - .expect("argument should have default value"); - let max_wait_seconds = - u64::from_str(max_wait_seconds).map_err(|_| AppleCodesignError::CliBadArgument)?; - - Ok(std::time::Duration::from_secs(max_wait_seconds)) -} - -fn command_notary_log(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let notarizer = notarizer_from_args(args)?; - let submission_id = args - .value_of("submission_id") - .expect("submission_id is required"); - - let log = notarizer.fetch_notarization_log(submission_id)?; - - for line in serde_json::to_string_pretty(&log)?.lines() { - println!("{}", line); - } - - Ok(()) -} - -fn command_notary_submit(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let path = PathBuf::from( - args.value_of("path") - .expect("clap should have validated arguments"), - ); - let staple = args.is_present("staple"); - let wait = args.is_present("wait") || staple; - - let wait_limit = if wait { - Some(notarizer_wait_duration(args)?) - } else { - None - }; - let notarizer = notarizer_from_args(args)?; - - let upload = notarizer.notarize_path(&path, wait_limit)?; - - if staple { - match upload { - crate::notarization::NotarizationUpload::UploadId(_) => { - panic!( - "NotarizationUpload::UploadId should not be returned if we waited successfully" - ); - } - crate::notarization::NotarizationUpload::NotaryResponse(_) => { - let stapler = crate::stapling::Stapler::new()?; - stapler.staple_path(&path)?; - } - } - } - - Ok(()) -} - -fn command_notary_wait(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let wait_duration = notarizer_wait_duration(args)?; - let notarizer = notarizer_from_args(args)?; - let submission_id = args - .value_of("submission_id") - .expect("submission_id is required"); - - notarizer.wait_on_notarization_and_fetch_log(submission_id, wait_duration)?; - - Ok(()) -} - -fn command_parse_code_signing_requirement(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let path = args - .value_of("input_path") - .expect("clap should have validated argument"); - - let data = std::fs::read(path)?; - - let requirements = CodeRequirements::parse_blob(&data)?.0; - - for requirement in requirements.iter() { - match args - .value_of("format") - .expect("clap should have validated argument") - { - "csrl" => { - println!("{}", requirement); - } - "expression-tree" => { - println!("{:#?}", requirement); - } - format => panic!("unhandled format: {}", format), - } - } - - Ok(()) -} - -fn command_print_signature_info(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let path = args - .value_of("path") - .expect("clap should have validated argument"); - - let reader = SignatureReader::from_path(path)?; - - let entities = reader.entities()?; - serde_yaml::to_writer(std::io::stdout(), &entities)?; - - Ok(()) -} - -fn command_remote_sign(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let remote_url = args - .value_of("remote_signing_url") - .expect("remote signing URL should always be present"); - - let session_join_string = if args.is_present("session_join_string_editor") { - let mut value = None; - - for _ in 0..3 { - if let Some(content) = dialoguer::Editor::new() - .require_save(true) - .edit("# Please enter the -----BEGIN SESSION JOIN STRING---- content below.\n# Remember to save the file!")? - { - value = Some(content); - break; - } - } - - value.ok_or_else(|| { - AppleCodesignError::CliGeneralError("session join string not entered in editor".into()) - })? - } else if let Some(path) = args.value_of("session_join_string_path") { - std::fs::read_to_string(path)? - } else if let Some(value) = args.value_of("session_join_string") { - value.to_string() - } else { - return Err(AppleCodesignError::CliGeneralError( - "session join string argument parsing failure".into(), - )); - }; - - let mut joiner = create_session_joiner(session_join_string)?; - - if let Some(env) = args.value_of("remote_shared_secret_env") { - let secret = std::env::var(env).map_err(|_| AppleCodesignError::CliBadArgument)?; - joiner.register_state(SessionJoinState::SharedSecret(secret.as_bytes().to_vec()))?; - } else if let Some(secret) = args.value_of("remote_shared_secret") { - joiner.register_state(SessionJoinState::SharedSecret(secret.as_bytes().to_vec()))?; - } - - let (private_keys, mut public_certificates) = collect_certificates_from_args(args, true)?; - - let private = private_keys - .into_iter() - .next() - .ok_or(AppleCodesignError::NoSigningCertificate)?; - - let cert = public_certificates.remove(0); - - let certificates = if let Some(chain) = cert.apple_root_certificate_chain() { - // The chain starts with self. - chain.into_iter().skip(1).collect::>() - } else { - public_certificates - }; - - joiner.register_state(SessionJoinState::PublicKeyDecrypt( - private.to_public_key_peer_decrypt()?, - ))?; - - let client = UnjoinedSigningClient::new_signer( - joiner, - private.as_key_info_signer(), - cert, - certificates, - remote_url.to_string(), - )?; - client.run()?; - - Ok(()) -} - -fn command_sign(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let mut settings = SigningSettings::default(); - - let (private_keys, mut public_certificates) = collect_certificates_from_args(args, true)?; - - if private_keys.len() > 1 { - error!("at most 1 PRIVATE KEY can be present; aborting"); - return Err(AppleCodesignError::CliBadArgument); - } - - let private = if private_keys.is_empty() { - None - } else { - Some(&private_keys[0]) - }; - - if let Some(signing_key) = &private { - if public_certificates.is_empty() { - error!("a PRIVATE KEY requires a corresponding CERTIFICATE to pair with it"); - return Err(AppleCodesignError::CliBadArgument); - } - - let cert = public_certificates.remove(0); - - warn!("registering signing key"); - settings.set_signing_key(signing_key.as_key_info_signer(), cert); - if let Some(certs) = settings.chain_apple_certificates() { - for cert in certs { - warn!( - "automatically registered Apple CA certificate: {}", - cert.subject_common_name() - .unwrap_or_else(|| "default".into()) - ); - } - } - - if let Some(timestamp_url) = args.value_of("timestamp_url") { - if timestamp_url != "none" { - warn!("using time-stamp protocol server {}", timestamp_url); - settings.set_time_stamp_url(timestamp_url)?; - } - } - } - - if let Some(team_id) = settings.set_team_id_from_signing_certificate() { - warn!( - "automatically setting team ID from signing certificate: {}", - team_id - ); - } - - for cert in public_certificates { - warn!("registering extra X.509 certificate"); - settings.chain_certificate(cert); - } - - if let Some(team_name) = args.value_of("team_name") { - settings.set_team_id(team_name); - } - - if let Some(value) = args.value_of("digest") { - let digest_type = DigestType::try_from(value)?; - settings.set_digest_type(digest_type); - } - - if let Some(values) = args.values_of("extra_digest") { - for value in values { - let (scope, digest_type) = parse_scoped_value(value)?; - let digest_type = DigestType::try_from(digest_type)?; - settings.add_extra_digest(scope, digest_type); - } - } - - if let Some(values) = args.values_of("exclude") { - for pattern in values { - settings.add_path_exclusion(pattern)?; - } - } - - if let Some(values) = args.values_of("binary_identifier") { - for value in values { - let (scope, identifier) = parse_scoped_value(value)?; - settings.set_binary_identifier(scope, identifier); - } - } - - if let Some(values) = args.values_of("code_requirements_path") { - for value in values { - let (scope, path) = parse_scoped_value(value)?; - - let code_requirements_data = std::fs::read(path)?; - let reqs = CodeRequirements::parse_blob(&code_requirements_data)?.0; - for expr in reqs.iter() { - warn!( - "setting designated code requirements for {}: {}", - scope, expr - ); - settings.set_designated_requirement_expression(scope.clone(), expr)?; - } - } - } - - if let Some(values) = args.values_of("code_resources") { - for value in values { - let (scope, path) = parse_scoped_value(value)?; - - warn!( - "setting code resources data for {} from path {}", - scope, path - ); - let code_resources_data = std::fs::read(path)?; - settings.set_code_resources_data(scope, code_resources_data); - } - } - - if let Some(values) = args.values_of("code_signature_flags_set") { - for value in values { - let (scope, value) = parse_scoped_value(value)?; - - let flags = CodeSignatureFlags::from_str(value)?; - settings.set_code_signature_flags(scope, flags); - } - } - - if let Some(values) = args.values_of("entitlements_xml_path") { - for value in values { - let (scope, path) = parse_scoped_value(value)?; - - warn!("setting entitlments XML for {} from path {}", scope, path); - let entitlements_data = std::fs::read_to_string(path)?; - settings.set_entitlements_xml(scope, entitlements_data)?; - } - } - - if let Some(values) = args.values_of("runtime_version") { - for value in values { - let (scope, value) = parse_scoped_value(value)?; - - let version = semver::Version::parse(value)?; - settings.set_runtime_version(scope, version); - } - } - - if let Some(values) = args.values_of("info_plist_path") { - for value in values { - let (scope, value) = parse_scoped_value(value)?; - - let content = std::fs::read(value)?; - settings.set_info_plist_data(scope, content); - } - } - - let input_path = PathBuf::from( - args.value_of("input_path") - .expect("input_path presence should have been validated by clap"), - ); - let output_path = args.value_of("output_path"); - - let signer = UnifiedSigner::new(settings); - - if let Some(output_path) = output_path { - warn!("signing {} to {}", input_path.display(), output_path); - signer.sign_path(input_path, output_path)?; - } else { - warn!("signing {} in place", input_path.display()); - signer.sign_path_in_place(input_path)?; - } - - if let Some(private) = &private { - private.finish()?; - } - - Ok(()) -} - -#[cfg(feature = "yubikey")] -fn command_smartcard_scan(_args: &ArgMatches) -> Result<(), AppleCodesignError> { - let mut ctx = ::yubikey::reader::Context::open()?; - for (index, reader) in ctx.iter()?.enumerate() { - println!("Device {}: {}", index, reader.name()); - - if let Ok(yk) = reader.open() { - let mut yk = yubikey::YubiKey::from(yk); - println!("Device {}: Serial: {}", index, yk.inner()?.serial()); - println!("Device {}: Version: {}", index, yk.inner()?.version()); - - for (slot, cert) in yk.find_certificates()? { - println!( - "Device {}: Certificate in slot {:?} / {}", - index, - slot, - hex::encode(&[u8::from(slot)]) - ); - print_certificate_info(&cert)?; - println!(); - } - } - } - - Ok(()) -} - -#[cfg(not(feature = "yubikey"))] -fn command_smartcard_scan(_args: &ArgMatches) -> Result<(), AppleCodesignError> { - eprintln!("smartcard reading requires the `yubikey` crate feature, which isn't enabled."); - eprintln!("recompile the crate with `cargo build --features yubikey` to enable support"); - std::process::exit(1); -} - -#[cfg(feature = "yubikey")] -fn command_smartcard_generate_key(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let slot_id = - ::yubikey::piv::SlotId::from_str(args.value_of("smartcard_slot").ok_or_else(|| { - error!("--smartcard-slot is required"); - AppleCodesignError::CliBadArgument - })?)?; - - let touch_policy = str_to_touch_policy( - args.value_of("touch_policy") - .expect("touch_policy argument is required"), - )?; - let pin_policy = str_to_pin_policy( - args.value_of("pin_policy") - .expect("pin_policy argument is required"), - )?; - - let mut yk = YubiKey::new()?; - yk.set_pin_callback(prompt_smartcard_pin); - - yk.generate_key(slot_id, touch_policy, pin_policy)?; - - Ok(()) -} - -#[cfg(not(feature = "yubikey"))] -fn command_smartcard_generate_key(_args: &ArgMatches) -> Result<(), AppleCodesignError> { - eprintln!("smartcard integration requires the `yubikey` crate feature, which isn't enabled."); - eprintln!("recompile the crate with `cargo build --features yubikey` to enable support"); - std::process::exit(1); -} - -#[cfg(feature = "yubikey")] -fn command_smartcard_import(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let (keys, certs) = collect_certificates_from_args(args, false)?; - - let slot_id = - ::yubikey::piv::SlotId::from_str(args.value_of("smartcard_slot").ok_or_else(|| { - error!("--smartcard-slot is required"); - AppleCodesignError::CliBadArgument - })?)?; - let touch_policy = str_to_touch_policy( - args.value_of("touch_policy") - .expect("touch_policy argument is required"), - )?; - let pin_policy = str_to_pin_policy( - args.value_of("pin_policy") - .expect("pin_policy argument is required"), - )?; - let use_existing_key = args.is_present("existing_key"); - - println!( - "found {} private keys and {} public certificates", - keys.len(), - certs.len() - ); - - let key = if use_existing_key { - println!("using existing private key in smartcard"); - - if !keys.is_empty() { - println!( - "ignoring {} private keys specified via arguments", - keys.len() - ); - } - - None - } else { - Some(keys.into_iter().next().ok_or_else(|| { - println!("no private key found"); - AppleCodesignError::CliBadArgument - })?) - }; - - let cert = certs.into_iter().next().ok_or_else(|| { - println!("no public certificates found"); - AppleCodesignError::CliBadArgument - })?; - - println!( - "Will import the following certificate into slot {}", - hex::encode([u8::from(slot_id)]) - ); - print_certificate_info(&cert)?; - - let mut yk = YubiKey::new()?; - yk.set_pin_callback(prompt_smartcard_pin); - - if args.is_present("dry_run") { - println!("dry run mode enabled; stopping"); - return Ok(()); - } - - if let Some(key) = key { - yk.import_key( - slot_id, - key.as_key_info_signer(), - &cert, - touch_policy, - pin_policy, - )?; - } else { - yk.import_certificate(slot_id, &cert)?; - } - - Ok(()) -} - -#[cfg(not(feature = "yubikey"))] -fn command_smartcard_import(_args: &ArgMatches) -> Result<(), AppleCodesignError> { - eprintln!("smartcard import requires `yubikey` crate feature, which isn't enabled."); - eprintln!("recompile the crate with `cargo build --features yubikey` to enable support"); - std::process::exit(1); -} - -fn command_staple(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let path = args - .value_of("path") - .ok_or(AppleCodesignError::CliBadArgument)?; - - let stapler = crate::stapling::Stapler::new()?; - stapler.staple_path(path)?; - - Ok(()) -} - -fn command_verify(args: &ArgMatches) -> Result<(), AppleCodesignError> { - let path = args - .value_of("path") - .ok_or(AppleCodesignError::CliBadArgument)?; - - let data = std::fs::read(path)?; - - let problems = verify::verify_macho_data(&data); - - for problem in &problems { - println!("{}", problem); - } - - if problems.is_empty() { - eprintln!("no problems detected!"); - eprintln!("(we do not verify everything so please do not assume that the signature meets Apple standards)"); - Ok(()) - } else { - Err(AppleCodesignError::VerificationProblems) - } -} - -fn command_x509_oids(_args: &ArgMatches) -> Result<(), AppleCodesignError> { - println!("# Extended Key Usage (EKU) Extension OIDs"); - println!(); - for ekup in crate::certificate::ExtendedKeyUsagePurpose::all() { - println!("{}\t{:?}", ekup.as_oid(), ekup); - } - println!(); - println!("# Code Signing Certificate Extension OIDs"); - println!(); - for ext in crate::certificate::CodeSigningCertificateExtension::all() { - println!("{}\t{:?}", ext.as_oid(), ext); - } - println!(); - println!("# Certificate Authority Certificate Extension OIDs"); - println!(); - for ext in crate::certificate::CertificateAuthorityExtension::all() { - println!("{}\t{:?}", ext.as_oid(), ext); - } - - Ok(()) -} - -fn main_impl() -> Result<(), AppleCodesignError> { - let app = Command::new("Cross platform Apple code signing in pure Rust") - .version(env!("CARGO_PKG_VERSION")) - .author("Gregory Szorc ") - .about("Sign and notarize Apple programs. See https://gregoryszorc.com/docs/apple-codesign/main/ for more docs.") - .arg_required_else_help(true) - .arg( - Arg::new("verbose") - .long("verbose") - .short('v') - .global(true) - .multiple_occurrences(true) - .help("Increase logging verbosity. Can be specified multiple times."), - ); - - let app = app.subcommand(add_certificate_source_args( - Command::new("analyze-certificate") - .about("Analyze an X.509 certificate for Apple code signing properties") - .long_about(ANALYZE_CERTIFICATE_ABOUT), - )); - - let app = app.subcommand( - Command::new("compute-code-hashes") - .about("Compute code hashes for a binary") - .arg( - Arg::new("path") - .required(true) - .help("path to Mach-O binary to examine"), - ) - .arg( - Arg::new("hash") - .long("hash") - .takes_value(true) - .possible_values(SUPPORTED_HASHES) - .default_value("sha256") - .help("Hashing algorithm to use"), - ) - .arg( - Arg::new("page_size") - .long("page-size") - .takes_value(true) - .default_value("4096") - .help("Chunk size to digest over"), - ) - .arg( - Arg::new("universal_index") - .long("universal-index") - .takes_value(true) - .default_value("0") - .help("Index of Mach-O binary to operate on within a universal/fat binary"), - ), - ); - - let app = app.subcommand( - Command::new("diff-signatures") - .about("Print a diff between the signature content of two paths") - .arg( - Arg::new("path0") - .required(true) - .help("The first path to compare"), - ) - .arg( - Arg::new("path1") - .required(true) - .help("The second path to compare"), - ), - ); - - let app = app.subcommand( - Command::new("encode-app-store-connect-api-key") - .about("Encode App Store Connect API Key metadata to a single file") - .long_about(ENCODE_APP_STORE_CONNECT_API_KEY_ABOUT) - .arg( - Arg::new("output_path") - .short('o') - .long("output-path") - .takes_value(true) - .allow_invalid_utf8(true) - .help("Path to a JSON file to create the output to"), - ) - .arg( - Arg::new("issuer_id") - .required(true) - .help("The issuer of the API Token. Likely a UUID"), - ) - .arg( - Arg::new("key_id") - .required(true) - .help("The Key ID. A short alphanumeric string like DEADBEEF42"), - ) - .arg( - Arg::new("private_key_path") - .required(true) - .allow_invalid_utf8(true) - .help("Path to a file containing the private key downloaded from Apple"), - ), - ); - - let app = app.subcommand( - Command::new("extract") - .about("Extracts code signature data from a Mach-O binary") - .long_about(EXTRACT_ABOUT) - .arg( - Arg::new("path") - .required(true) - .help("Path to Mach-O binary to examine"), - ) - .arg( - Arg::new("data") - .long("data") - .takes_value(true) - .possible_values(&[ - "blobs", - "cms-info", - "cms-pem", - "cms-raw", - "cms", - "code-directory-raw", - "code-directory-serialized-raw", - "code-directory-serialized", - "code-directory", - "linkedit-info", - "linkedit-segment-raw", - "macho-load-commands", - "macho-segments", - "macho-target", - "requirements-raw", - "requirements-rust", - "requirements-serialized-raw", - "requirements-serialized", - "requirements", - "signature-raw", - "superblob", - ]) - .default_value("linkedit-info") - .help("Which data to extract and how to format it"), - ) - .arg( - Arg::new("universal_index") - .long("universal-index") - .takes_value(true) - .default_value("0") - .help("Index of Mach-O binary to operate on within a universal/fat binary"), - ), - ); - - let app = app.subcommand( - add_certificate_source_args(Command::new("generate-certificate-signing-request") - .about("Generates a certificate signing request that can be sent to Apple and exchanged for a signing certificate") - .arg( - Arg::new("csr_pem_path") - .long("csr-pem-path") - .takes_value(true) - .help("Path to file to write PEM encoded CSR to") - ) - )); - - let app = app.subcommand( - Command::new("generate-self-signed-certificate") - .about("Generate a self-signed certificate for code signing") - .long_about(GENERATE_SELF_SIGNED_CERTIFICATE_ABOUT) - .arg( - Arg::new("algorithm") - .long("algorithm") - .takes_value(true) - .possible_values(&["ecdsa", "ed25519"]) - .default_value("ecdsa") - .help("Which key type to use"), - ) - .arg( - Arg::new("profile") - .long("profile") - .takes_value(true) - .possible_values(CertificateProfile::str_names()) - .default_value("apple-development"), - ) - .arg( - Arg::new("team_id") - .long("team-id") - .takes_value(true) - .default_value("unset") - .help( - "Team ID (this is a short string attached to your Apple Developer account)", - ), - ) - .arg( - Arg::new("person_name") - .long("person-name") - .takes_value(true) - .required(true) - .help("The name of the person this certificate is for"), - ) - .arg( - Arg::new("country_name") - .long("country-name") - .takes_value(true) - .default_value("XX") - .help("Country Name (C) value for certificate identifier"), - ) - .arg( - Arg::new("validity_days") - .long("validity-days") - .takes_value(true) - .default_value("365") - .help("How many days the certificate should be valid for"), - ) - .arg( - Arg::new("pem_filename") - .long("pem-filename") - .takes_value(true) - .help("Base name of files to write PEM encoded certificate to"), - ), - ); - - let app = app. - subcommand(Command::new("keychain-export-certificate-chain") - .about("Export Apple CA certificates from the macOS Keychain") - .arg( - Arg::new("domain") - .long("domain") - .possible_values(&["user", "system", "common", "dynamic"]) - .default_value("user") - .help("Keychain domain to operate on") - ) - .arg( - Arg::new("password") - .long("--password") - .takes_value(true) - .help("Password to unlock the Keychain") - ) - .arg( - Arg::new("password_file") - .long("--password-file") - .takes_value(true) - .conflicts_with("password") - .help("File containing password to use to unlock the Keychain") - ) - .arg( - Arg::new("no_print_self") - .long("--no-print-self") - .help("Print only the issuing certificate chain, not the subject certificate") - ) - .arg( - Arg::new("user_id") - .long("--user-id") - .takes_value(true) - .required(true) - .help("User ID value of code signing certificate to find and whose CA chain to export") - ), - ); - - let app = app.subcommand( - Command::new("keychain-print-certificates") - .about("Print information about certificates in the macOS keychain") - .arg( - Arg::new("domain") - .long("--domain") - .possible_values(&["user", "system", "common", "dynamic"]) - .default_value("user") - .help("Keychain domain to operate on"), - ), - ); - - let app = app.subcommand(add_notary_api_args( - Command::new("notary-log") - .about("Fetch the notarization log for a previous submission") - .arg( - Arg::new("submission_id") - .required(true) - .takes_value(true) - .help("The ID of the previous submission to wait on"), - ), - )); - - let app = - app.subcommand(add_notary_api_args( - Command::new("notary-submit") - .about("Upload an asset to Apple for notarization and possibly staple it") - .long_about(NOTARIZE_ABOUT) - .alias("notarize") - .arg( - Arg::new("wait") - .long("wait") - .help("Whether to wait for upload processing to complete"), - ) - .arg( - Arg::new("max_wait_seconds") - .long("max-wait-seconds") - .takes_value(true) - .default_value("600") - .help("Maximum time in seconds to wait for the upload result"), - ) - .arg(Arg::new("staple").long("staple").help( - "Staple the notarization ticket after successful upload (implies --wait)", - )) - .arg( - Arg::new("path") - .takes_value(true) - .required(true) - .help("Path to asset to upload"), - ), - )); - - let app = app.subcommand(add_notary_api_args( - Command::new("notary-wait") - .about("Wait for completion of a previous submission") - .arg( - Arg::new("max_wait_seconds") - .long("max-wait-seconds") - .takes_value(true) - .default_value("600") - .help("Maximum time in seconds to wait for the upload result"), - ) - .arg( - Arg::new("submission_id") - .required(true) - .takes_value(true) - .help("The ID of the previous submission to wait on"), - ), - )); - - let app = app.subcommand( - Command::new("parse-code-signing-requirement") - .about("Parse binary Code Signing Requirement data into a human readable string") - .long_about(PARSE_CODE_SIGNING_REQUIREMENT_ABOUT) - .arg( - Arg::new("format") - .long("--format") - .required(true) - .possible_values(&["csrl", "expression-tree"]) - .default_value("csrl") - .help("Output format"), - ) - .arg( - Arg::new("input_path") - .required(true) - .help("Path to file to parse"), - ), - ); - - let mut app = app.subcommand( - Command::new("print-signature-info") - .about("Print signature information for a filesystem path") - .arg( - Arg::new("path") - .required(true) - .help("Filesystem path to entity whose info to print"), - ), - ); - - if cfg!(feature = "yubikey") { - app = app.subcommand( - Command::new("smartcard-scan") - .about("Show information about available smartcard (SC) devices"), - ); - - app = app.subcommand(add_yubikey_policy_args( - Command::new("smartcard-generate-key") - .about("Generate a new private key on a smartcard") - .arg( - Arg::new("smartcard_slot") - .long("smartcard-slot") - .takes_value(true) - .required(true) - .help("Smartcard slot number to store key in (9c is common)"), - ), - )); - - app = app.subcommand(add_yubikey_policy_args(add_certificate_source_args( - Command::new("smartcard-import") - .about("Import a code signing certificate and key into a smartcard") - .arg( - Arg::new("existing_key") - .long("existing-key") - .help("Re-use the existing private key in the smartcard slot"), - ) - .arg( - Arg::new("dry_run") - .long("dry-run") - .help("Don't actually perform the import"), - ), - ))); - } - - let app = app.subcommand(add_certificate_source_args( - Command::new("remote-sign") - .about("Create signatures initiated from a remote signing operation") - .arg( - Arg::new("session_join_string_editor") - .long("editor") - .help("Open an editor to input the session join string"), - ) - .arg( - Arg::new("session_join_string_path") - .long("sjs-path") - .takes_value(true) - .help("Path to file containing session join string"), - ) - .arg( - Arg::new("session_join_string") - .takes_value(true) - .help("Session join string (provided by the signing initiator)"), - ) - .group( - ArgGroup::new("session_join_string_source") - .arg("session_join_string_editor") - .arg("session_join_string_path") - .arg("session_join_string") - .required(true), - ), - )); - - let app = app - .subcommand( - add_certificate_source_args(Command::new("sign") - .about("Sign a Mach-O binary or bundle") - .long_about(SIGN_ABOUT) - .arg( - Arg::new("binary_identifier") - .long("binary-identifier") - .takes_value(true) - .multiple_occurrences(true) - .multiple_values(true) - .number_of_values(1) - .help("Identifier string for binary. The value normally used by CFBundleIdentifier") - ) - .arg( - Arg::new("code_requirements_path") - .long("code-requirements-path") - .takes_value(true) - .multiple_occurrences(true) - .multiple_values(true) - .number_of_values(1) - .help("Path to a file containing binary code requirements data to be used as designated requirements") - ) - .arg( - Arg::new("code_resources") - .long("code-resources-path") - .takes_value(true) - .multiple_occurrences(true) - .multiple_values(true) - .number_of_values(1) - .help("Path to an XML plist file containing code resources"), - ) - .arg( - Arg::new("code_signature_flags_set") - .long("code-signature-flags") - .takes_value(true) - .multiple_occurrences(true) - .multiple_values(true) - .number_of_values(1) - .possible_values(CodeSignatureFlags::all_user_configurable()) - .help("Code signature flags to set") - ) - .arg( - Arg::new("digest") - .long("digest") - .possible_values(SUPPORTED_HASHES) - .takes_value(true) - .default_value("sha256") - .help("Digest algorithm to use") - ) - .arg(Arg::new("extra_digest") - .long("extra-digest") - .possible_values(SUPPORTED_HASHES) - .takes_value(true) - .multiple_occurrences(true) - .multiple_values(true) - .number_of_values(1).help("Extra digests to include in signatures") - ) - .arg( - Arg::new("entitlements_xml_path") - .long("entitlements-xml-path") - .short('e') - .takes_value(true) - .multiple_occurrences(true) - .multiple_values(true) - .number_of_values(1) - .help("Path to a plist file containing entitlements"), - ) - .arg( - Arg::new("runtime_version") - .long("runtime-version") - .takes_value(true) - .multiple_occurrences(true) - .multiple_values(true) - .number_of_values(1) - .help("Hardened runtime version to use (defaults to SDK version used to build binary)")) - .arg( - Arg::new("info_plist_path") - .long("info-plist-path") - .takes_value(true) - .multiple_occurrences(true) - .multiple_values(true) - .number_of_values(1) - .help("Path to an Info.plist file whose digest to include in Mach-O signature") - ) - .arg( - Arg::new( - "team_name") - .long("team-name") - .takes_value(true) - .help("Team name/identifier to include in code signature" - ) - ) - .arg( - Arg::new("timestamp_url") - .long("timestamp-url") - .takes_value(true) - .default_value(APPLE_TIMESTAMP_URL) - .help( - "URL of timestamp server to use to obtain a token of the CMS signature", - ), - ) - .arg( - Arg::new("exclude") - .long("exclude") - .takes_value(true) - .multiple_occurrences(true) - .multiple_values(true) - .number_of_values(1) - .help("Glob expression of paths to exclude from signing") - ) - .arg( - Arg::new("input_path") - .required(true) - .help("Path to Mach-O binary to sign"), - ) - .arg( - Arg::new("output_path") - .help("Path to signed Mach-O binary to write"), - ), - )); - - let app = app.subcommand( - Command::new("staple") - .about("Staples a notarization ticket to an entity") - .arg( - Arg::new("path") - .required(true) - .help("Path to entity to attempt to staple"), - ), - ); - - let app = app.subcommand( - Command::new("verify") - .about("Verifies code signature data") - .arg( - Arg::new("path") - .required(true) - .help("Path of Mach-O binary to examine"), - ), - ); - - let app = app.subcommand( - Command::new("x509-oids") - .about("Print information about X.509 OIDs related to Apple code signing"), - ); - - let matches = app.get_matches(); - - // TODO make default log level warn once we audit logging sites. - let log_level = match matches.occurrences_of("verbose") { - 0 => LevelFilter::Info, - 1 => LevelFilter::Debug, - _ => LevelFilter::Trace, - }; - - let mut builder = env_logger::Builder::from_env( - env_logger::Env::default().default_filter_or(log_level.as_str()), - ); - - // Disable log context except at higher log levels. - if log_level <= LevelFilter::Info { - builder - .format_timestamp(None) - .format_level(false) - .format_target(false); - } - - // This spews unwanted output at default level. Nerf it by default. - if log_level == LevelFilter::Info { - builder.filter_module("rustls", LevelFilter::Error); - } - - builder.init(); - - match matches.subcommand() { - Some(("analyze-certificate", args)) => command_analyze_certificate(args), - Some(("compute-code-hashes", args)) => command_compute_code_hashes(args), - Some(("diff-signatures", args)) => command_diff_signatures(args), - Some(("encode-app-store-connect-api-key", args)) => { - command_encode_app_store_connect_api_key(args) - } - Some(("extract", args)) => command_extract(args), - Some(("generate-certificate-signing-request", args)) => { - command_generate_certificate_signing_request(args) - } - Some(("generate-self-signed-certificate", args)) => { - command_generate_self_signed_certificate(args) - } - Some(("keychain-export-certificate-chain", args)) => { - command_keychain_export_certificate_chain(args) - } - Some(("keychain-print-certificates", args)) => command_keychain_print_certificates(args), - Some(("notary-log", args)) => command_notary_log(args), - Some(("notary-submit", args)) => command_notary_submit(args), - Some(("notary-wait", args)) => command_notary_wait(args), - Some(("parse-code-signing-requirement", args)) => { - command_parse_code_signing_requirement(args) - } - Some(("print-signature-info", args)) => command_print_signature_info(args), - Some(("remote-sign", args)) => command_remote_sign(args), - Some(("sign", args)) => command_sign(args), - Some(("smartcard-generate-key", args)) => command_smartcard_generate_key(args), - Some(("smartcard-import", args)) => command_smartcard_import(args), - Some(("smartcard-scan", args)) => command_smartcard_scan(args), - Some(("staple", args)) => command_staple(args), - Some(("verify", args)) => command_verify(args), - Some(("x509-oids", args)) => command_x509_oids(args), - _ => Err(AppleCodesignError::CliUnknownCommand), - } -} - -fn main() { - let exit_code = match main_impl() { - Ok(()) => 0, - Err(err) => { - eprintln!("Error: {}", err); - 1 - } - }; - - std::process::exit(exit_code) -} diff --git a/apple-codesign/src/notarization.rs b/apple-codesign/src/notarization.rs deleted file mode 100644 index ab328d4e6..000000000 --- a/apple-codesign/src/notarization.rs +++ /dev/null @@ -1,458 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -/*! Apple notarization functionality. - -Notarization works by uploading a payload to Apple servers and waiting for -Apple to scan the submitted content. If Apple is appeased by your submission, -they issue a notarization ticket, which can be downloaded and *stapled* (just -a fancy word for *attached*) to the content you upload. - -This module implements functionality for uploading content to Apple -and waiting on the availability of a notarization ticket. -*/ - -use { - crate::{ - app_store_connect::{ - api_token::ConnectTokenEncoder, - notary_api::{ - NewSubmissionResponse, NotaryApiClient, SubmissionResponse, - SubmissionResponseStatus, - }, - AppStoreConnectClient, - }, - reader::PathType, - AppleCodesignError, - }, - apple_bundles::DirectoryBundle, - aws_sdk_s3::{Credentials, Region}, - aws_smithy_http::byte_stream::ByteStream, - log::{info, warn}, - sha2::Digest, - std::{ - fs::File, - io::{Read, Seek, SeekFrom, Write}, - path::{Path, PathBuf}, - time::Duration, - }, -}; - -fn digest(reader: &mut R) -> Result<(u64, Vec), AppleCodesignError> { - let mut hasher = H::new(); - let mut size = 0; - - loop { - let mut buffer = [0u8; 16384]; - let count = reader.read(&mut buffer)?; - - size += count as u64; - hasher.update(&buffer[0..count]); - - if count < buffer.len() { - break; - } - } - - Ok((size, hasher.finalize().to_vec())) -} - -fn digest_sha256(reader: &mut R) -> Result<(u64, Vec), AppleCodesignError> { - digest::(reader) -} - -/// Produce zip file data from a [DirectoryBundle]. -/// -/// The built zip file will contain all the files from the bundle under a directory -/// tree having the bundle name. e.g. if you pass `MyApp.app`, the zip will have -/// files like `MyApp.app/Contents/Info.plist`. -pub fn bundle_to_zip(bundle: &DirectoryBundle) -> Result, AppleCodesignError> { - let mut zf = zip::ZipWriter::new(std::io::Cursor::new(vec![])); - - let mut symlinks = vec![]; - - for file in bundle - .files(true) - .map_err(AppleCodesignError::DirectoryBundle)? - { - let entry = file - .as_file_entry() - .map_err(AppleCodesignError::DirectoryBundle)?; - - let name = - format!("{}/{}", bundle.name(), file.relative_path().display()).replace('\\', "/"); - - let options = zip::write::FileOptions::default(); - - let options = if entry.link_target().is_some() { - symlinks.push(name.as_bytes().to_vec()); - options.compression_method(zip::CompressionMethod::Stored) - } else if entry.is_executable() { - options.unix_permissions(0o755) - } else { - options.unix_permissions(0o644) - }; - - zf.start_file(name, options)?; - - if let Some(target) = entry.link_target() { - zf.write_all(target.to_string_lossy().replace('\\', "/").as_bytes())?; - } else { - zf.write_all(&entry.resolve_content()?)?; - } - } - - let mut writer = zf.finish()?; - - // Current versions of the zip crate don't support writing symlinks. We - // added that support upstream but it isn't released yet. - // TODO remove this hackery once we upgrade the zip crate. - let eocd = zip_structs::zip_eocd::ZipEOCD::from_reader(&mut writer)?; - let cd_entries = - zip_structs::zip_central_directory::ZipCDEntry::all_from_eocd(&mut writer, &eocd)?; - - for mut cd in cd_entries { - if symlinks.contains(&cd.file_name_raw) { - cd.external_file_attributes = - (0o120777 << 16) | (cd.external_file_attributes & 0x0000ffff); - writer.seek(SeekFrom::Start(cd.starting_position_with_signature))?; - cd.write(&mut writer)?; - } - } - - Ok(writer.into_inner()) -} - -/// Represents the result of a notarization upload. -pub enum NotarizationUpload { - /// We performed the upload and only have the upload ID / UUID for it. - /// - /// (We probably didn't wait for the upload to finish processing.) - UploadId(String), - - /// We performed an upload and have upload state from the server. - NotaryResponse(SubmissionResponse), -} - -enum UploadKind { - Data(Vec), - Path(PathBuf), -} - -/// An entity for performing notarizations. -/// -/// Notarization works by uploading content to Apple, waiting for Apple to inspect -/// and react to that upload, then downloading a notarization "ticket" from Apple -/// and incorporating it into the entity being signed. -#[derive(Clone)] -pub struct Notarizer { - token_encoder: Option, - - /// How long to wait between polling the server for upload status. - wait_poll_interval: Duration, -} - -impl Notarizer { - /// Construct a new instance. - pub fn new() -> Result { - Ok(Self { - token_encoder: None, - wait_poll_interval: Duration::from_secs(3), - }) - } - - /// Define the App Store Connect JWT token encoder to use. - /// - /// This is the most generic way to define the credentials for this client. - pub fn set_token_encoder(&mut self, encoder: ConnectTokenEncoder) { - self.token_encoder = Some(encoder); - } - - /// Set the API key used to upload. - /// - /// The API issuer is required when using an API key. - pub fn set_api_key( - &mut self, - api_issuer: impl ToString, - api_key: impl ToString, - ) -> Result<(), AppleCodesignError> { - let api_key = api_key.to_string(); - let api_issuer = api_issuer.to_string(); - - let encoder = ConnectTokenEncoder::from_api_key_id(api_key, api_issuer)?; - - self.set_token_encoder(encoder); - - Ok(()) - } - - /// Attempt to notarize an asset defined by a filesystem path. - /// - /// The type of path is sniffed out and the appropriate notarization routine is called. - pub fn notarize_path( - &self, - path: &Path, - wait_limit: Option, - ) -> Result { - match PathType::from_path(path)? { - PathType::Bundle => { - let bundle = DirectoryBundle::new_from_path(path) - .map_err(AppleCodesignError::DirectoryBundle)?; - self.notarize_bundle(&bundle, wait_limit) - } - PathType::Xar => self.notarize_flat_package(path, wait_limit), - PathType::Dmg => self.notarize_dmg(path, wait_limit), - PathType::MachO | PathType::Other => Err(AppleCodesignError::NotarizeUnsupportedPath( - path.to_path_buf(), - )), - } - } - - /// Attempt to notarize an on-disk bundle. - /// - /// If `wait_limit` is provided, we will wait for the upload to finish processing. - /// Otherwise, this returns as soon as the upload is performed. - pub fn notarize_bundle( - &self, - bundle: &DirectoryBundle, - wait_limit: Option, - ) -> Result { - let zipfile = bundle_to_zip(bundle)?; - let digest = sha2::Sha256::digest(&zipfile); - - let submission = self.create_submission(&digest, &format!("{}.zip", bundle.name()))?; - - self.upload_s3_and_maybe_wait(submission, UploadKind::Data(zipfile), wait_limit) - } - - /// Attempt to notarize a DMG file. - pub fn notarize_dmg( - &self, - dmg_path: &Path, - wait_limit: Option, - ) -> Result { - let filename = dmg_path - .file_name() - .map(|x| x.to_string_lossy().to_string()) - .unwrap_or_else(|| "dmg".to_string()); - - let (_, digest) = digest_sha256(&mut File::open(dmg_path)?)?; - - let submission = self.create_submission(&digest, &filename)?; - - self.upload_s3_and_maybe_wait( - submission, - UploadKind::Path(dmg_path.to_path_buf()), - wait_limit, - ) - } - - /// Attempt to notarize a flat package (`.pkg`) installer. - pub fn notarize_flat_package( - &self, - pkg_path: &Path, - wait_limit: Option, - ) -> Result { - let filename = pkg_path - .file_name() - .map(|x| x.to_string_lossy().to_string()) - .unwrap_or_else(|| "pkg".to_string()); - - let (_, digest) = digest_sha256(&mut File::open(pkg_path)?)?; - - let submission = self.create_submission(&digest, &filename)?; - - self.upload_s3_and_maybe_wait( - submission, - UploadKind::Path(pkg_path.to_path_buf()), - wait_limit, - ) - } -} - -impl Notarizer { - /// Tell the notary service to expect an upload to S3. - fn create_submission( - &self, - raw_digest: &[u8], - name: &str, - ) -> Result { - let client = match &self.token_encoder { - Some(token) => Ok(NotaryApiClient::from(AppStoreConnectClient::new( - token.clone(), - )?)), - _ => Err(AppleCodesignError::NotarizeNoAuthCredentials), - }?; - - let digest = hex::encode(raw_digest); - warn!( - "creating Notary API submission for {} (sha256: {})", - name, digest - ); - - let submission = client.create_submission(&digest, name)?; - - warn!("created submission ID: {}", submission.data.id); - - Ok(submission) - } - - fn upload_s3_package( - &self, - submission: &NewSubmissionResponse, - upload: UploadKind, - ) -> Result<(), AppleCodesignError> { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - let bytestream = match upload { - UploadKind::Data(data) => ByteStream::from(data), - UploadKind::Path(path) => rt.block_on(ByteStream::from_path(path))?, - }; - - // upload using s3 api - warn!("resolving AWS S3 configuration from Apple-provided credentials"); - let config = rt.block_on( - aws_config::from_env() - .credentials_provider(Credentials::new( - submission.data.attributes.aws_access_key_id.clone(), - submission.data.attributes.aws_secret_access_key.clone(), - Some(submission.data.attributes.aws_session_token.clone()), - None, - "apple-codesign", - )) - // The region is not given anywhere in the Apple documentation. From - // manually testing all available regions, it appears to be - // us-west-2. - .region(Region::new("us-west-2")) - .load(), - ); - - let s3_client = aws_sdk_s3::Client::new(&config); - - warn!( - "uploading asset to s3://{}/{}", - submission.data.attributes.bucket, submission.data.attributes.object - ); - info!("(you may see additional log output from S3 client)"); - - // TODO: Support multi-part upload. - // Unfortunately, aws-sdk-s3 does not have a simple upload_file helper - // like it does in other languages. - // See https://github.com/awslabs/aws-sdk-rust/issues/494 - let fut = s3_client - .put_object() - .bucket(submission.data.attributes.bucket.clone()) - .key(submission.data.attributes.object.clone()) - .body(bytestream) - .send(); - - rt.block_on(fut).map_err(aws_sdk_s3::Error::from)?; - - warn!("S3 upload completed successfully"); - - Ok(()) - } - - fn upload_s3_and_maybe_wait( - &self, - submission: NewSubmissionResponse, - upload_data: UploadKind, - wait_limit: Option, - ) -> Result { - self.upload_s3_package(&submission, upload_data)?; - - let status = if let Some(wait_limit) = wait_limit { - self.wait_on_notarization_and_fetch_log(&submission.data.id, wait_limit)? - } else { - return Ok(NotarizationUpload::UploadId(submission.data.id)); - }; - - // Make sure notarization was successful. - let status = status.into_result()?; - - Ok(NotarizationUpload::NotaryResponse(status)) - } - - pub fn wait_on_notarization( - &self, - submission_id: &str, - wait_limit: Duration, - ) -> Result { - warn!( - "waiting up to {}s for package upload {} to finish processing", - wait_limit.as_secs(), - submission_id - ); - - let start_time = std::time::Instant::now(); - - loop { - let client = match &self.token_encoder { - Some(token) => Ok(NotaryApiClient::from(AppStoreConnectClient::new( - token.clone(), - )?)), - None => Err(AppleCodesignError::NotarizeNoAuthCredentials), - }?; - - let status = client.get_submission(submission_id)?; - - let elapsed = start_time.elapsed(); - - info!( - "poll state after {}s: {:?}", - elapsed.as_secs(), - status.data.attributes.status - ); - - if status.data.attributes.status != SubmissionResponseStatus::InProgress { - warn!("Notary API Server has finished processing the uploaded asset"); - - return Ok(status); - } - - if elapsed >= wait_limit { - warn!("reached wait limit after {}s", elapsed.as_secs()); - return Err(AppleCodesignError::NotarizeWaitLimitReached); - } - - std::thread::sleep(self.wait_poll_interval); - } - } - - /// Obtain the processing log from an upload. - pub fn fetch_notarization_log( - &self, - submission_id: &str, - ) -> Result { - warn!("fetching notarization log for {}", submission_id); - let client = match &self.token_encoder { - Some(token) => Ok(NotaryApiClient::from(AppStoreConnectClient::new( - token.clone(), - )?)), - None => Err(AppleCodesignError::NotarizeNoAuthCredentials), - }?; - client.get_submission_log(submission_id) - } - - /// Waits on an app store package upload and fetches and logs the upload log. - /// - /// This is just a convenience around [Self::wait_on_app_store_package_upload()] and - /// [Self::fetch_upload_log()]. - pub fn wait_on_notarization_and_fetch_log( - &self, - submission_id: &str, - wait_limit: Duration, - ) -> Result { - let status = self.wait_on_notarization(submission_id, wait_limit)?; - - let log = self.fetch_notarization_log(submission_id)?; - - for line in serde_json::to_string_pretty(&log)?.lines() { - warn!("notary log> {}", line); - } - - Ok(status) - } -} diff --git a/apple-codesign/src/policy.rs b/apple-codesign/src/policy.rs deleted file mode 100644 index ffae227e7..000000000 --- a/apple-codesign/src/policy.rs +++ /dev/null @@ -1,306 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Apple trust policies. -//! -//! Apple operating systems have a number of pre-canned trust policies -//! that must be fulfilled in order to trust signed code. These are -//! often based off the presence of specific X.509 certificates in the -//! issuing chain and/or the presence of attributes in X.509 certificates. -//! -//! Trust policies are often engraved in code signatures as part of the -//! signed code requirements expression. -//! -//! This module defines a bunch of metadata for describing Apple trust -//! entities and also provides pre-canned policies that can be easily -//! constructed to match those employed by Apple's official signing tools. -//! -//! Apple's certificates can be found at -//! . - -use { - crate::{ - certificate::{ - AppleCertificate, CertificateAuthorityExtension, CertificateProfile, - CodeSigningCertificateExtension, - }, - code_requirement::{CodeRequirementExpression, CodeRequirementMatchExpression}, - error::AppleCodesignError, - }, - once_cell::sync::Lazy, - std::ops::Deref, - x509_certificate::CapturedX509Certificate, -}; - -/// Code signing requirement for Mac Developer ID. -/// -/// `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.14] or certificate leaf[field.1.2.840.113635.100.6.1.13])` -static POLICY_MAC_DEVELOPER_ID: Lazy> = Lazy::new(|| { - CodeRequirementExpression::And( - Box::new(CodeRequirementExpression::And( - Box::new(CodeRequirementExpression::AnchorAppleGeneric), - Box::new(CodeRequirementExpression::CertificateGeneric( - 1, - CertificateAuthorityExtension::DeveloperId.as_oid(), - CodeRequirementMatchExpression::Exists, - )), - )), - Box::new(CodeRequirementExpression::Or( - Box::new(CodeRequirementExpression::CertificateGeneric( - 0, - CodeSigningCertificateExtension::DeveloperIdInstaller.as_oid(), - CodeRequirementMatchExpression::Exists, - )), - Box::new(CodeRequirementExpression::CertificateGeneric( - 0, - CodeSigningCertificateExtension::DeveloperIdApplication.as_oid(), - CodeRequirementMatchExpression::Exists, - )), - )), - ) -}); - -/// Notarized executable. -/// -/// `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 notarized'` -/// -static POLICY_NOTARIZED_EXECUTABLE: Lazy> = Lazy::new(|| { - CodeRequirementExpression::And( - Box::new(CodeRequirementExpression::And( - Box::new(CodeRequirementExpression::And( - Box::new(CodeRequirementExpression::AnchorAppleGeneric), - Box::new(CodeRequirementExpression::CertificateGeneric( - 1, - CertificateAuthorityExtension::DeveloperId.as_oid(), - CodeRequirementMatchExpression::Exists, - )), - )), - Box::new(CodeRequirementExpression::CertificateGeneric( - 0, - CodeSigningCertificateExtension::DeveloperIdApplication.as_oid(), - CodeRequirementMatchExpression::Exists, - )), - )), - Box::new(CodeRequirementExpression::Notarized), - ) -}); - -/// Notarized installer. -/// -/// `'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.14] or certificate -/// leaf[field.1.2.840.113635.100.6.1.13]) and notarized'` -static POLICY_NOTARIZED_INSTALLER: Lazy> = Lazy::new(|| { - CodeRequirementExpression::And( - Box::new(CodeRequirementExpression::And( - Box::new(CodeRequirementExpression::And( - Box::new(CodeRequirementExpression::AnchorAppleGeneric), - Box::new(CodeRequirementExpression::CertificateGeneric( - 1, - CertificateAuthorityExtension::DeveloperId.as_oid(), - CodeRequirementMatchExpression::Exists, - )), - )), - Box::new(CodeRequirementExpression::Or( - Box::new(CodeRequirementExpression::CertificateGeneric( - 0, - CodeSigningCertificateExtension::DeveloperIdInstaller.as_oid(), - CodeRequirementMatchExpression::Exists, - )), - Box::new(CodeRequirementExpression::CertificateGeneric( - 0, - CodeSigningCertificateExtension::DeveloperIdApplication.as_oid(), - CodeRequirementMatchExpression::Exists, - )), - )), - )), - Box::new(CodeRequirementExpression::Notarized), - ) -}); - -/// Defines well-known execution policies for signed code. -/// -/// Instances can be obtained from a human-readable string for convenience. Those -/// strings are: -/// -/// * `developer-id-signed` -/// * `developer-id-notarized-executable` -/// * `developer-id-notarized-installer` -#[allow(clippy::enum_variant_names)] -pub enum ExecutionPolicy { - /// Code is signed by a certificate authorized for signing Mac applications or - /// installers and that certificate was issued by - /// [crate::apple_certificates::KnownCertificate::DeveloperIdG1] or - /// [crate::apple_certificates::KnownCertificate::DeveloperIdG2]. - /// - /// This is the policy that applies when you get a `Developer ID Application` or - /// `Developer ID Installer` certificate from Apple. - DeveloperIdSigned, - - /// Like [Self::DeveloperIdSigned] but only applies to executables (not installers) - /// and the executable must be notarized. - /// - /// If you notarize an individual executable, you effectively convert the - /// [Self::DeveloperIdSigned] policy into this variant. - DeveloperIdNotarizedExecutable, - - /// Like [Self::DeveloperIdSigned] but only applies to installers (not executables) - /// and the installer must be notarized. - /// - /// If you notarize an individual installer, you effectively convert the - /// [Self::DeveloperIdSigned] policy into this variant. - DeveloperIdNotarizedInstaller, -} - -impl Deref for ExecutionPolicy { - type Target = CodeRequirementExpression<'static>; - - fn deref(&self) -> &Self::Target { - match self { - Self::DeveloperIdSigned => POLICY_MAC_DEVELOPER_ID.deref(), - Self::DeveloperIdNotarizedExecutable => POLICY_NOTARIZED_EXECUTABLE.deref(), - Self::DeveloperIdNotarizedInstaller => POLICY_NOTARIZED_INSTALLER.deref(), - } - } -} - -impl TryFrom<&str> for ExecutionPolicy { - type Error = AppleCodesignError; - - fn try_from(s: &str) -> Result { - match s { - "developer-id-signed" => Ok(Self::DeveloperIdSigned), - "developer-id-notarized-executable" => Ok(Self::DeveloperIdNotarizedExecutable), - "developer-id-notarized-installer" => Ok(Self::DeveloperIdNotarizedInstaller), - _ => Err(AppleCodesignError::UnknownPolicy(s.to_string())), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn get_policies() { - ExecutionPolicy::DeveloperIdSigned.to_bytes().unwrap(); - ExecutionPolicy::DeveloperIdNotarizedExecutable - .to_bytes() - .unwrap(); - ExecutionPolicy::DeveloperIdNotarizedInstaller - .to_bytes() - .unwrap(); - } -} - -/// Derive a designated requirements expression given a code signing certificate. -/// -/// This function figures out what the run-time requirements of a signed binary -/// should be given its code signing certificate. -/// -/// We determine the flavor of Apple code signing certificate in use and apply an -/// appropriate requirements policy. We strive for behavior equivalence with -/// Apple's `codesign` tool. -pub fn derive_designated_requirements( - cert: &CapturedX509Certificate, - identifier: Option, -) -> Result>, AppleCodesignError> { - let profile = if let Some(profile) = cert.apple_guess_profile() { - profile - } else { - return Ok(None); - }; - - match profile { - // These appear to be the same policy. - CertificateProfile::AppleDevelopment | CertificateProfile::AppleDistribution => { - let cn = cert.subject_common_name().ok_or_else(|| { - AppleCodesignError::PolicyFormulationError(format!( - "(deriving for {:?}) certificate common name not available", - profile - )) - })?; - - let expr = CodeRequirementExpression::And( - // It chains to Apple root CA. - Box::new(CodeRequirementExpression::AnchorAppleGeneric), - Box::new(CodeRequirementExpression::And( - // It was signed by this cert. - Box::new(CodeRequirementExpression::CertificateField( - 0, - "subject.CN".to_string().into(), - CodeRequirementMatchExpression::Equal(cn.into()), - )), - // That cert was signed by a CA with WWDR extension. - Box::new(CodeRequirementExpression::CertificateGeneric( - 1, - CertificateAuthorityExtension::AppleWorldwideDeveloperRelations.as_oid(), - CodeRequirementMatchExpression::Exists, - )), - )), - ); - - Ok(Some(if let Some(identifier) = identifier { - CodeRequirementExpression::And( - Box::new(CodeRequirementExpression::Identifier(identifier.into())), - Box::new(expr), - ) - } else { - expr - })) - } - CertificateProfile::DeveloperIdApplication => { - let team_id = cert.apple_team_id().ok_or_else(|| { - AppleCodesignError::PolicyFormulationError(format!( - "(deriving for {:?}) could not find team identifier in signing certificate", - profile - )) - })?; - - let expr = CodeRequirementExpression::And( - // Chains to Apple root CA. - Box::new(CodeRequirementExpression::AnchorAppleGeneric), - Box::new(CodeRequirementExpression::And( - // Certificate issued by CA with Developer ID extension. - Box::new(CodeRequirementExpression::CertificateGeneric( - 1, - CertificateAuthorityExtension::DeveloperId.as_oid(), - CodeRequirementMatchExpression::Exists, - )), - Box::new(CodeRequirementExpression::And( - // A certificate entrusted with Developer ID Application signing rights. - Box::new(CodeRequirementExpression::CertificateGeneric( - 0, - CodeSigningCertificateExtension::DeveloperIdApplication.as_oid(), - CodeRequirementMatchExpression::Exists, - )), - // Signed by this team ID. - Box::new(CodeRequirementExpression::CertificateField( - 0, - "subject.OU".to_string().into(), - CodeRequirementMatchExpression::Equal(team_id.into()), - )), - )), - )), - ); - - Ok(Some(if let Some(identifier) = identifier { - CodeRequirementExpression::And( - Box::new(CodeRequirementExpression::Identifier(identifier.into())), - Box::new(expr), - ) - } else { - expr - })) - } - CertificateProfile::MacInstallerDistribution | CertificateProfile::DeveloperIdInstaller => { - Err(AppleCodesignError::PolicyFormulationError(format!( - "(deriving for {:?}) we do not know how to handle this policy", - profile - ))) - } - } -} diff --git a/apple-codesign/src/reader.rs b/apple-codesign/src/reader.rs deleted file mode 100644 index bcb988ac7..000000000 --- a/apple-codesign/src/reader.rs +++ /dev/null @@ -1,996 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Functionality for reading signature data from files. - -use { - crate::{ - certificate::AppleCertificate, - code_directory::CodeDirectoryBlob, - dmg::{path_is_dmg, DmgReader}, - embedded_signature::{BlobEntry, DigestType, EmbeddedSignature}, - embedded_signature_builder::{CD_DIGESTS_OID, CD_DIGESTS_PLIST_OID}, - error::AppleCodesignError, - macho::{MachFile, MachOBinary}, - }, - apple_bundles::{DirectoryBundle, DirectoryBundleFile}, - apple_xar::{ - reader::XarReader, - table_of_contents::{ - ChecksumType as XarChecksumType, File as XarTocFile, Signature as XarTocSignature, - }, - }, - cryptographic_message_syntax::{SignedData, SignerInfo}, - goblin::mach::{fat::FAT_MAGIC, parse_magic_and_ctx}, - serde::Serialize, - std::{ - fmt::Debug, - fs::File, - io::{BufWriter, Cursor, Read, Seek}, - ops::Deref, - path::{Path, PathBuf}, - }, - x509_certificate::{CapturedX509Certificate, DigestAlgorithm}, -}; - -enum MachOType { - Mach, - MachO, -} - -impl MachOType { - pub fn from_path(path: impl AsRef) -> Result, AppleCodesignError> { - let mut fh = File::open(path.as_ref())?; - - let mut header = vec![0u8; 4]; - let count = fh.read(&mut header)?; - - if count < 4 { - return Ok(None); - } - - let magic = goblin::mach::peek(&header, 0)?; - - match magic { - FAT_MAGIC => Ok(Some(Self::Mach)), - _ if parse_magic_and_ctx(&header, 0).is_ok() => Ok(Some(Self::MachO)), - _ => Ok(None), - } - } -} - -/// Test whether a given path is likely a XAR file. -pub fn path_is_xar(path: impl AsRef) -> Result { - let mut fh = File::open(path.as_ref())?; - - let mut header = [0u8; 4]; - - let count = fh.read(&mut header)?; - if count < 4 { - Ok(false) - } else { - Ok(header.as_ref() == b"xar!") - } -} - -/// Describes the type of entity at a path. -/// -/// This represents a best guess. -pub enum PathType { - MachO, - Dmg, - Bundle, - Xar, - Other, -} - -impl PathType { - /// Attempt to classify the type of signable entity based on a filesystem path. - pub fn from_path(path: impl AsRef) -> Result { - let path = path.as_ref(); - - if path.is_file() { - if path_is_dmg(path)? { - Ok(PathType::Dmg) - } else if path_is_xar(path)? { - Ok(PathType::Xar) - } else { - match MachOType::from_path(path)? { - Some(MachOType::Mach | MachOType::MachO) => Ok(Self::MachO), - None => Ok(Self::Other), - } - } - } else if path.is_dir() { - Ok(PathType::Bundle) - } else { - Ok(PathType::Other) - } - } -} - -fn pretty_print_xml(xml: &[u8]) -> Result, AppleCodesignError> { - let mut reader = xml::reader::EventReader::new(Cursor::new(xml)); - let mut emitter = xml::EmitterConfig::new() - .perform_indent(true) - .create_writer(BufWriter::new(Vec::with_capacity(xml.len() * 2))); - - while let Ok(event) = reader.next() { - match event { - xml::reader::XmlEvent::EndDocument => { - break; - } - xml::reader::XmlEvent::Whitespace(_) => {} - event => { - if let Some(event) = event.as_writer_event() { - emitter.write(event).map_err(AppleCodesignError::XmlWrite)?; - } - } - } - } - - let xml = emitter.into_inner().into_inner().map_err(|e| { - AppleCodesignError::Io(std::io::Error::new(std::io::ErrorKind::BrokenPipe, e)) - })?; - - Ok(xml) -} - -#[derive(Clone, Debug, Serialize)] -pub struct BlobDescription { - pub slot: String, - pub magic: String, - pub length: u32, - pub sha1: String, - pub sha256: String, -} - -impl<'a> From<&BlobEntry<'a>> for BlobDescription { - fn from(entry: &BlobEntry<'a>) -> Self { - Self { - slot: format!("{:?}", entry.slot), - magic: format!("{:x}", u32::from(entry.magic)), - length: entry.length as _, - sha1: hex::encode( - entry - .digest_with(DigestType::Sha1) - .expect("sha-1 digest should always work"), - ), - sha256: hex::encode( - entry - .digest_with(DigestType::Sha256) - .expect("sha-256 digest should always work"), - ), - } - } -} - -#[derive(Clone, Debug, Serialize)] -pub struct CertificateInfo { - pub subject: String, - pub issuer: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub key_algorithm: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub signature_algorithm: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub signed_with_algorithm: Option, - pub is_apple_root_ca: bool, - pub is_apple_intermediate_ca: bool, - pub chains_to_apple_root_ca: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub apple_ca_extension: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub apple_extended_key_usages: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub apple_code_signing_extensions: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub apple_certificate_profile: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub apple_team_id: Option, -} - -impl TryFrom<&CapturedX509Certificate> for CertificateInfo { - type Error = AppleCodesignError; - - fn try_from(cert: &CapturedX509Certificate) -> Result { - Ok(Self { - subject: cert - .subject_name() - .user_friendly_str() - .map_err(AppleCodesignError::CertificateDecode)?, - issuer: cert - .issuer_name() - .user_friendly_str() - .map_err(AppleCodesignError::CertificateDecode)?, - key_algorithm: cert.key_algorithm().map(|x| x.to_string()), - signature_algorithm: cert.signature_algorithm().map(|x| x.to_string()), - signed_with_algorithm: cert.signature_signature_algorithm().map(|x| x.to_string()), - is_apple_root_ca: cert.is_apple_root_ca(), - is_apple_intermediate_ca: cert.is_apple_intermediate_ca(), - chains_to_apple_root_ca: cert.chains_to_apple_root_ca(), - apple_ca_extension: cert.apple_ca_extension().map(|x| x.to_string()), - apple_extended_key_usages: cert - .apple_extended_key_usage_purposes() - .into_iter() - .map(|x| x.to_string()) - .collect::>(), - apple_code_signing_extensions: cert - .apple_code_signing_extensions() - .into_iter() - .map(|x| x.to_string()) - .collect::>(), - apple_certificate_profile: cert.apple_guess_profile().map(|x| x.to_string()), - apple_team_id: cert.apple_team_id(), - }) - } -} - -#[derive(Clone, Debug, Serialize)] -pub struct CmsSigner { - pub issuer: String, - pub digest_algorithm: String, - pub signature_algorithm: String, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub attributes: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub content_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub message_digest: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub signing_time: Option>, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub cdhash_plist: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub cdhash_digests: Vec<(String, String)>, - pub signature_verifies: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub time_stamp_token: Option, -} - -impl CmsSigner { - pub fn from_signer_info_and_signed_data( - signer_info: &SignerInfo, - signed_data: &SignedData, - ) -> Result { - let mut attributes = vec![]; - let mut content_type = None; - let mut message_digest = None; - let mut signing_time = None; - let mut time_stamp_token = None; - let mut cdhash_plist = vec![]; - let mut cdhash_digests = vec![]; - - if let Some(sa) = signer_info.signed_attributes() { - content_type = Some(sa.content_type().to_string()); - message_digest = Some(hex::encode(sa.message_digest())); - if let Some(t) = sa.signing_time() { - signing_time = Some(*t); - } - - for attr in sa.attributes().iter() { - attributes.push(format!("{}", attr.typ)); - - if attr.typ == CD_DIGESTS_PLIST_OID { - if let Some(data) = attr.values.get(0) { - let data = data.deref().clone(); - - let plist = data - .decode(|cons| { - let v = bcder::OctetString::take_from(cons)?; - - Ok(v.into_bytes()) - }) - .map_err(|e| AppleCodesignError::Cms(e.into()))?; - - cdhash_plist = String::from_utf8_lossy(&pretty_print_xml(&plist)?) - .lines() - .map(|x| x.to_string()) - .collect::>(); - } - } else if attr.typ == CD_DIGESTS_OID { - for value in &attr.values { - // Each value is a SEQUENECE of (OID, OctetString). - let data = value.deref().clone(); - - data.decode(|cons| { - while let Some(_) = cons.take_opt_sequence(|cons| { - let oid = bcder::Oid::take_from(cons)?; - let value = bcder::OctetString::take_from(cons)?; - - cdhash_digests - .push((format!("{}", oid), hex::encode(value.into_bytes()))); - - Ok(()) - })? {} - - Ok(()) - }) - .map_err(|e| AppleCodesignError::Cms(e.into()))?; - } - } - } - } - - // The order should matter per RFC 5652 but Apple's CMS implementation doesn't - // conform to spec. - attributes.sort(); - - if let Some(tsk) = signer_info.time_stamp_token_signed_data()? { - time_stamp_token = Some(tsk.try_into()?); - } - - Ok(Self { - issuer: signer_info - .certificate_issuer_and_serial() - .expect("issuer should always be set") - .0 - .user_friendly_str() - .map_err(AppleCodesignError::CertificateDecode)?, - digest_algorithm: signer_info.digest_algorithm().to_string(), - signature_algorithm: signer_info.signature_algorithm().to_string(), - attributes, - content_type, - message_digest, - signing_time, - cdhash_plist, - cdhash_digests, - signature_verifies: signer_info - .verify_signature_with_signed_data(signed_data) - .is_ok(), - - time_stamp_token, - }) - } -} - -/// High-level representation of a CMS signature. -#[derive(Clone, Debug, Serialize)] -pub struct CmsSignature { - #[serde(skip_serializing_if = "Vec::is_empty")] - pub certificates: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub signers: Vec, -} - -impl TryFrom for CmsSignature { - type Error = AppleCodesignError; - - fn try_from(signed_data: SignedData) -> Result { - let certificates = signed_data - .certificates() - .map(|x| x.try_into()) - .collect::, _>>()?; - - let signers = signed_data - .signers() - .map(|x| CmsSigner::from_signer_info_and_signed_data(x, &signed_data)) - .collect::, _>>()?; - - Ok(Self { - certificates, - signers, - }) - } -} - -#[derive(Clone, Debug, Serialize)] -pub struct CodeDirectory { - pub version: String, - pub flags: String, - pub identifier: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub team_name: Option, - pub digest_type: String, - pub platform: u8, - pub signed_entity_size: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub executable_segment_flags: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub runtime_version: Option, - pub code_digests_count: usize, - #[serde(skip_serializing_if = "Vec::is_empty")] - slot_digests: Vec, -} - -impl<'a> TryFrom> for CodeDirectory { - type Error = AppleCodesignError; - - fn try_from(cd: CodeDirectoryBlob<'a>) -> Result { - let mut temp = cd - .slot_digests() - .iter() - .map(|(slot, digest)| (slot, digest.as_hex())) - .collect::>(); - temp.sort_by(|(a, _), (b, _)| a.cmp(b)); - - let slot_digests = temp - .into_iter() - .map(|(slot, digest)| format!("{:?}: {}", slot, digest)) - .collect::>(); - - Ok(Self { - version: format!("0x{:X}", cd.version), - flags: format!("{:?}", cd.flags), - identifier: cd.ident.to_string(), - team_name: cd.team_name.map(|x| x.to_string()), - signed_entity_size: cd.code_limit as _, - digest_type: format!("{}", cd.digest_type), - platform: cd.platform, - executable_segment_flags: cd.exec_seg_flags.map(|x| format!("{:?}", x)), - runtime_version: cd - .runtime - .map(|x| format!("{}", crate::macho::parse_version_nibbles(x))), - code_digests_count: cd.code_digests.len(), - slot_digests, - }) - } -} - -/// High level representation of a code signature. -#[derive(Clone, Debug, Serialize)] -pub struct CodeSignature { - /// Length of the code signature data. - pub superblob_length: u32, - pub blob_count: u32, - pub blobs: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub code_directory: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub alternative_code_directories: Vec<(String, CodeDirectory)>, - pub entitlements_plist: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub code_requirements: Vec, - pub cms: Option, -} - -impl<'a> TryFrom> for CodeSignature { - type Error = AppleCodesignError; - - fn try_from(sig: EmbeddedSignature<'a>) -> Result { - let mut entitlements_plist = None; - let mut code_requirements = vec![]; - let mut cms = None; - - let code_directory = if let Some(cd) = sig.code_directory()? { - Some(CodeDirectory::try_from(*cd)?) - } else { - None - }; - - let alternative_code_directories = sig - .alternate_code_directories()? - .into_iter() - .map(|(slot, cd)| Ok((format!("{:?}", slot), CodeDirectory::try_from(*cd)?))) - .collect::, AppleCodesignError>>()?; - - if let Some(blob) = sig.entitlements()? { - entitlements_plist = Some(blob.as_str().to_string()); - } - - if let Some(req) = sig.code_requirements()? { - let mut temp = vec![]; - - for (req, blob) in req.requirements { - let reqs = blob.parse_expressions()?; - temp.push((req, format!("{}", reqs))); - } - - temp.sort_by(|(a, _), (b, _)| a.cmp(b)); - - code_requirements = temp - .into_iter() - .map(|(req, value)| format!("{}: {}", req, value)) - .collect::>(); - } - - if let Some(signed_data) = sig.signed_data()? { - cms = Some(signed_data.try_into()?); - } - - Ok(Self { - superblob_length: sig.length, - blob_count: sig.count, - blobs: sig - .blobs - .iter() - .map(BlobDescription::from) - .collect::>(), - code_directory, - alternative_code_directories, - entitlements_plist, - code_requirements, - cms, - }) - } -} - -#[derive(Clone, Debug, Default, Serialize)] -pub struct MachOEntity { - pub linkedit_segment_file_start_offset: Option, - pub linkedit_segment_file_end_offset: Option, - pub signature_file_start_offset: Option, - pub signature_file_end_offset: Option, - pub signature_linkedit_start_offset: Option, - pub signature_linkedit_end_offset: Option, - pub signature: Option, -} - -#[derive(Clone, Debug, Serialize)] -pub struct DmgEntity { - pub code_signature_offset: u64, - pub code_signature_size: u64, - pub signature: Option, -} - -#[derive(Clone, Debug, Serialize)] -pub enum CodeSignatureFile { - ResourcesXml(Vec), - NotarizationTicket, - Other, -} - -#[derive(Clone, Debug, Serialize)] -pub struct XarTableOfContents { - pub toc_length_compressed: u64, - pub toc_length_uncompressed: u64, - pub checksum_offset: u64, - pub checksum_size: u64, - pub checksum_type: String, - pub toc_start_offset: u16, - pub heap_start_offset: u64, - pub creation_time: String, - pub toc_checksum_reported: String, - pub toc_checksum_reported_sha1_digest: String, - pub toc_checksum_reported_sha256_digest: String, - pub toc_checksum_actual_sha1: String, - pub toc_checksum_actual_sha256: String, - pub checksum_verifies: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub signature: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub x_signature: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub xml: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub rsa_signature: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub rsa_signature_verifies: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cms_signature: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cms_signature_verifies: Option, -} - -impl XarTableOfContents { - pub fn from_xar( - xar: &mut XarReader, - ) -> Result { - let (digest_type, digest) = xar.checksum()?; - let _xml = xar.table_of_contents_decoded_data()?; - - let (rsa_signature, rsa_signature_verifies) = if let Some(sig) = xar.rsa_signature()? { - ( - Some(hex::encode(&sig.0)), - Some(xar.verify_rsa_checksum_signature().unwrap_or(false)), - ) - } else { - (None, None) - }; - let (cms_signature, cms_signature_verifies) = - if let Some(signed_data) = xar.cms_signature()? { - ( - Some(CmsSignature::try_from(signed_data)?), - Some(xar.verify_cms_signature().unwrap_or(false)), - ) - } else { - (None, None) - }; - - let toc_checksum_actual_sha1 = xar.digest_table_of_contents_with(XarChecksumType::Sha1)?; - let toc_checksum_actual_sha256 = - xar.digest_table_of_contents_with(XarChecksumType::Sha256)?; - - let checksum_verifies = xar.verify_table_of_contents_checksum().unwrap_or(false); - - let header = xar.header(); - let toc = xar.table_of_contents(); - let checksum_offset = toc.checksum.offset; - let checksum_size = toc.checksum.size; - - // This can be useful for debugging. - //let xml = String::from_utf8_lossy(&pretty_print_xml(&xml)?) - // .lines() - // .map(|x| x.to_string()) - // .collect::>(); - let xml = vec![]; - - Ok(Self { - toc_length_compressed: header.toc_length_compressed, - toc_length_uncompressed: header.toc_length_uncompressed, - checksum_offset, - checksum_size, - checksum_type: apple_xar::format::XarChecksum::from(header.checksum_algorithm_id) - .to_string(), - toc_start_offset: header.size, - heap_start_offset: xar.heap_start_offset(), - creation_time: toc.creation_time.clone(), - toc_checksum_reported: format!("{}:{}", digest_type, hex::encode(&digest)), - toc_checksum_reported_sha1_digest: hex::encode(DigestType::Sha1.digest_data(&digest)?), - toc_checksum_reported_sha256_digest: hex::encode( - DigestType::Sha256.digest_data(&digest)?, - ), - toc_checksum_actual_sha1: hex::encode(&toc_checksum_actual_sha1), - toc_checksum_actual_sha256: hex::encode(&toc_checksum_actual_sha256), - checksum_verifies, - signature: if let Some(sig) = &toc.signature { - Some(sig.try_into()?) - } else { - None - }, - x_signature: if let Some(sig) = &toc.x_signature { - Some(sig.try_into()?) - } else { - None - }, - xml, - rsa_signature, - rsa_signature_verifies, - cms_signature, - cms_signature_verifies, - }) - } -} - -#[derive(Clone, Debug, Serialize)] -pub struct XarSignature { - pub style: String, - pub offset: u64, - pub size: u64, - pub end_offset: u64, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub certificates: Vec, -} - -impl TryFrom<&XarTocSignature> for XarSignature { - type Error = AppleCodesignError; - - fn try_from(sig: &XarTocSignature) -> Result { - Ok(Self { - style: sig.style.to_string(), - offset: sig.offset, - size: sig.size, - end_offset: sig.offset + sig.size, - certificates: sig - .x509_certificates()? - .into_iter() - .map(|cert| CertificateInfo::try_from(&cert)) - .collect::, AppleCodesignError>>()?, - }) - } -} - -#[derive(Clone, Debug, Default, Serialize)] -pub struct XarFile { - pub id: u64, - pub file_type: String, - pub data_size: Option, - pub data_length: Option, - pub data_extracted_checksum: Option, - pub data_archived_checksum: Option, - pub data_encoding: Option, -} - -impl TryFrom<&XarTocFile> for XarFile { - type Error = AppleCodesignError; - - fn try_from(file: &XarTocFile) -> Result { - let mut v = Self { - id: file.id, - file_type: file.file_type.to_string(), - ..Default::default() - }; - - if let Some(data) = &file.data { - v.populate_data(data); - } - - Ok(v) - } -} - -impl XarFile { - pub fn populate_data(&mut self, data: &apple_xar::table_of_contents::FileData) { - self.data_size = Some(data.size); - self.data_length = Some(data.length); - self.data_extracted_checksum = Some(format!( - "{}:{}", - data.extracted_checksum.style, data.extracted_checksum.checksum - )); - self.data_archived_checksum = Some(format!( - "{}:{}", - data.archived_checksum.style, data.archived_checksum.checksum - )); - self.data_encoding = Some(data.encoding.style.clone()); - } -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum SignatureEntity { - MachO(MachOEntity), - Dmg(DmgEntity), - BundleCodeSignatureFile(CodeSignatureFile), - XarTableOfContents(XarTableOfContents), - XarMember(XarFile), - Other, -} - -#[derive(Clone, Debug, Serialize)] -pub struct FileEntity { - pub path: PathBuf, - #[serde(skip_serializing_if = "Option::is_none")] - pub file_size: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub file_sha256: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub symlink_target: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sub_path: Option, - pub entity: SignatureEntity, -} - -impl FileEntity { - /// Construct an instance from a [Path]. - pub fn from_path(path: &Path, report_path: Option<&Path>) -> Result { - let metadata = std::fs::symlink_metadata(path)?; - - let report_path = if let Some(p) = report_path { - p.to_path_buf() - } else { - path.to_path_buf() - }; - - let (file_size, file_sha256, symlink_target) = if metadata.is_symlink() { - (None, None, Some(std::fs::read_link(path)?)) - } else { - ( - Some(metadata.len()), - Some(hex::encode(DigestAlgorithm::Sha256.digest_path(path)?)), - None, - ) - }; - - Ok(Self { - path: report_path, - file_size, - file_sha256, - symlink_target, - sub_path: None, - entity: SignatureEntity::Other, - }) - } -} - -/// Entity for reading Apple code signature data. -pub enum SignatureReader { - Dmg(PathBuf, Box), - MachO(PathBuf, Vec), - Bundle(Box), - FlatPackage(PathBuf), -} - -impl SignatureReader { - /// Construct a signature reader from a path. - pub fn from_path(path: impl AsRef) -> Result { - let path = path.as_ref(); - match PathType::from_path(path)? { - PathType::Bundle => Ok(Self::Bundle(Box::new( - DirectoryBundle::new_from_path(path) - .map_err(AppleCodesignError::DirectoryBundle)?, - ))), - PathType::Dmg => { - let mut fh = File::open(path)?; - Ok(Self::Dmg( - path.to_path_buf(), - Box::new(DmgReader::new(&mut fh)?), - )) - } - PathType::MachO => { - let data = std::fs::read(path)?; - MachFile::parse(&data)?; - - Ok(Self::MachO(path.to_path_buf(), data)) - } - PathType::Xar => Ok(Self::FlatPackage(path.to_path_buf())), - PathType::Other => Err(AppleCodesignError::UnrecognizedPathType), - } - } - - /// Obtain entities that are possibly relevant to code signing. - pub fn entities(&self) -> Result, AppleCodesignError> { - match self { - Self::Dmg(path, dmg) => { - let mut entity = FileEntity::from_path(path, None)?; - entity.entity = SignatureEntity::Dmg(Self::resolve_dmg_entity(dmg)?); - - Ok(vec![entity]) - } - Self::MachO(path, data) => Self::resolve_macho_entities_from_data(path, data, None), - Self::Bundle(bundle) => Self::resolve_bundle_entities(bundle), - Self::FlatPackage(path) => Self::resolve_flat_package_entities(path), - } - } - - fn resolve_dmg_entity(dmg: &DmgReader) -> Result { - let signature = if let Some(sig) = dmg.embedded_signature()? { - Some(sig.try_into()?) - } else { - None - }; - - Ok(DmgEntity { - code_signature_offset: dmg.koly().code_signature_offset, - code_signature_size: dmg.koly().code_signature_size, - signature, - }) - } - - fn resolve_macho_entities_from_data( - path: &Path, - data: &[u8], - report_path: Option<&Path>, - ) -> Result, AppleCodesignError> { - let mut entities = vec![]; - - let entity = FileEntity::from_path(path, report_path)?; - - for macho in MachFile::parse(data)?.into_iter() { - let mut entity = entity.clone(); - - if let Some(index) = macho.index { - entity.sub_path = Some(format!("macho-index:{}", index)); - } - - entity.entity = SignatureEntity::MachO(Self::resolve_macho_entity(macho)?); - - entities.push(entity); - } - - Ok(entities) - } - - fn resolve_macho_entity(macho: MachOBinary) -> Result { - let mut entity = MachOEntity::default(); - - if let Some(sig) = macho.find_signature_data()? { - entity.linkedit_segment_file_start_offset = Some(sig.linkedit_segment_start_offset); - entity.linkedit_segment_file_end_offset = Some(sig.linkedit_segment_end_offset); - entity.signature_file_start_offset = Some(sig.linkedit_signature_start_offset); - entity.signature_file_end_offset = Some(sig.linkedit_signature_end_offset); - entity.signature_linkedit_start_offset = Some(sig.signature_start_offset); - entity.signature_linkedit_end_offset = Some(sig.signature_end_offset); - } - - if let Some(sig) = macho.code_signature()? { - entity.signature = Some(sig.try_into()?); - } - - Ok(entity) - } - - fn resolve_bundle_entities( - bundle: &DirectoryBundle, - ) -> Result, AppleCodesignError> { - let mut entities = vec![]; - - for file in bundle - .files(true) - .map_err(AppleCodesignError::DirectoryBundle)? - { - entities.extend( - Self::resolve_bundle_file_entity(bundle.root_dir().to_path_buf(), file)? - .into_iter(), - ); - } - - Ok(entities) - } - - fn resolve_bundle_file_entity( - base_path: PathBuf, - file: DirectoryBundleFile, - ) -> Result, AppleCodesignError> { - let main_relative_path = match file.absolute_path().strip_prefix(&base_path) { - Ok(path) => path.to_path_buf(), - Err(_) => file.absolute_path().to_path_buf(), - }; - - let mut entities = vec![]; - - let mut default_entity = - FileEntity::from_path(file.absolute_path(), Some(&main_relative_path))?; - - let file_name = file - .absolute_path() - .file_name() - .expect("path should have file name") - .to_string_lossy(); - let parent_dir = file - .absolute_path() - .parent() - .expect("path should have parent directory"); - - // There may be bugs in the code identifying the role of files in bundles. - // So rely on our own heuristics to detect and report on the file type. - if default_entity.symlink_target.is_some() { - entities.push(default_entity); - } else if parent_dir.ends_with("_CodeSignature") { - if file_name == "CodeResources" { - let data = std::fs::read(file.absolute_path())?; - - default_entity.entity = - SignatureEntity::BundleCodeSignatureFile(CodeSignatureFile::ResourcesXml( - String::from_utf8_lossy(&data) - .split('\n') - .map(|x| x.replace('\t', " ")) - .collect::>(), - )); - - entities.push(default_entity); - } else { - default_entity.entity = - SignatureEntity::BundleCodeSignatureFile(CodeSignatureFile::Other); - - entities.push(default_entity); - } - } else if file_name == "CodeResources" { - default_entity.entity = - SignatureEntity::BundleCodeSignatureFile(CodeSignatureFile::NotarizationTicket); - - entities.push(default_entity); - } else { - let data = std::fs::read(file.absolute_path())?; - - match Self::resolve_macho_entities_from_data( - file.absolute_path(), - &data, - Some(&main_relative_path), - ) { - Ok(extra) => { - entities.extend(extra); - } - Err(_) => { - // Just some extra file. - entities.push(default_entity); - } - } - } - - Ok(entities) - } - - fn resolve_flat_package_entities(path: &Path) -> Result, AppleCodesignError> { - let mut xar = XarReader::new(File::open(path)?)?; - - let default_entity = FileEntity::from_path(path, None)?; - - let mut entities = vec![]; - - let mut entity = default_entity.clone(); - entity.sub_path = Some("toc".to_string()); - entity.entity = - SignatureEntity::XarTableOfContents(XarTableOfContents::from_xar(&mut xar)?); - entities.push(entity); - - // Now emit entries for all files in table of contents. - for (name, file) in xar.files()? { - let mut entity = default_entity.clone(); - entity.sub_path = Some(name); - entity.entity = SignatureEntity::XarMember(XarFile::try_from(&file)?); - entities.push(entity); - } - - Ok(entities) - } -} diff --git a/apple-codesign/src/remote_signing/mod.rs b/apple-codesign/src/remote_signing/mod.rs deleted file mode 100644 index 3e0dcc090..000000000 --- a/apple-codesign/src/remote_signing/mod.rs +++ /dev/null @@ -1,1084 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Remote signing support. - -pub mod session_negotiation; - -use { - crate::{ - cryptography::PrivateKey, - remote_signing::session_negotiation::{ - PeerKeys, PublicKeyPeerDecrypt, SessionInitiatePeer, SessionJoinContext, - SessionJoinPeerPreJoin, - }, - AppleCodesignError, - }, - bcder::{ - encode::{PrimitiveContent, Values}, - Mode, Oid, - }, - bytes::Bytes, - log::{debug, error, info, warn}, - serde::{de::DeserializeOwned, Deserialize, Serialize}, - signature::Signer, - std::{ - cell::{RefCell, RefMut}, - net::TcpStream, - }, - thiserror::Error, - tungstenite::{ - client::IntoClientRequest, - protocol::{Message, WebSocket, WebSocketConfig}, - stream::MaybeTlsStream, - }, - x509_certificate::{ - CapturedX509Certificate, KeyAlgorithm, KeyInfoSigner, Sign, Signature, SignatureAlgorithm, - X509CertificateError, - }, -}; - -/// URL of default server to use. -pub const DEFAULT_SERVER_URL: &str = "wss://ws.codesign.gregoryszorc.com/"; - -/// An error specific to remote signing. -#[derive(Debug, Error)] -pub enum RemoteSignError { - #[error("unexpected message received from relay server: {0}")] - ServerUnexpectedMessage(String), - - #[error("error reported from relay server: {0}")] - ServerError(String), - - #[error("not compatible with relay server; try upgrading to a new release?")] - ServerIncompatible, - - #[error("cryptography error: {0}")] - Crypto(String), - - #[error("bad client state: {0}")] - ClientState(&'static str), - - #[error("joining state not wanted for this session type: {0}")] - SessionJoinUnwantedState(String), - - #[error("session join string error: {0}")] - SessionJoinString(String), - - #[error("base64 decode error: {0}")] - Base64(#[from] base64::DecodeError), - - #[error("I/O error: {0}")] - Io(#[from] std::io::Error), - - #[error("PEM encoding error: {0}")] - Pem(#[from] pem::PemError), - - #[error("JSON serialization error: {0}")] - SerdeJson(#[from] serde_json::Error), - - #[error("SPAKE error: {0}")] - Spake(spake2::Error), - - #[error("SPKI error: {0}")] - Spki(#[from] spki::Error), - - #[error("websocket error: {0}")] - Websocket(#[from] tungstenite::Error), - - #[error("X.509 certificate handler error: {0}")] - X509(#[from] X509CertificateError), -} - -#[derive(Clone, Copy, Debug, PartialEq, Serialize)] -#[serde(rename_all = "kebab-case")] -enum ApiMethod { - Hello, - CreateSession, - JoinSession, - SendMessage, - Goodbye, -} - -/// A websocket message sent from the client to the server. -#[derive(Clone, Debug, Serialize)] -struct ClientMessage { - /// Unique ID for this request. - request_id: String, - /// API method being called. - api: ApiMethod, - /// Payload for this method. - payload: Option, -} - -/// Payload for a [ClientMessage]. -#[derive(Clone, Debug, Serialize)] -#[serde(untagged)] -enum ClientPayload { - CreateSession { - session_id: String, - ttl: u64, - context: Option, - }, - JoinSession { - session_id: String, - context: Option, - }, - SendMessage { - session_id: String, - message: String, - }, - Goodbye { - session_id: String, - reason: Option, - }, -} - -#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "kebab-case")] -enum ServerMessageType { - Error, - Greeting, - SessionCreated, - SessionJoined, - MessageSent, - PeerMessage, - SessionClosed, -} - -/// Websocket message sent from server to client. -#[derive(Clone, Debug, Deserialize)] -struct ServerMessage { - /// ID of request responsible for this message. - request_id: Option, - /// The type of message. - #[serde(rename = "type")] - typ: ServerMessageType, - ttl: Option, - payload: Option, -} - -impl ServerMessage { - fn into_result(self) -> Result { - if self.typ == ServerMessageType::Error { - let error = self.as_error()?; - Err(RemoteSignError::ServerError(format!( - "{}: {}", - error.code, error.message - ))) - } else { - Ok(self) - } - } - - fn as_type( - &self, - message_type: ServerMessageType, - ) -> Result { - if self.typ == message_type { - if let Some(value) = &self.payload { - Ok(serde_json::from_value(value.clone())?) - } else { - Err(RemoteSignError::ClientState( - "no payload for requested type", - )) - } - } else { - Err(RemoteSignError::ClientState( - "requested payload for wrong message type", - )) - } - } - - fn as_error(&self) -> Result { - self.as_type::(ServerMessageType::Error) - } - - fn as_greeting(&self) -> Result { - self.as_type::(ServerMessageType::Greeting) - } - - fn as_session_joined(&self) -> Result { - self.as_type::(ServerMessageType::SessionJoined) - } - - fn as_peer_message(&self) -> Result { - self.as_type::(ServerMessageType::PeerMessage) - } - - fn as_session_closed(&self) -> Result { - self.as_type::(ServerMessageType::SessionClosed) - } -} - -/// Response messages seen from server. -#[derive(Clone, Debug, Deserialize)] -#[serde(untagged)] -enum ServerPayload { - Error(ServerError), - Greeting(ServerGreeting), - SessionJoined(ServerJoined), - PeerMessage(ServerPeerMessage), - SessionClosed(ServerSessionClosed), -} - -#[derive(Clone, Debug, Deserialize)] -struct ServerError { - code: String, - message: String, -} - -#[derive(Clone, Debug, Deserialize)] -struct ServerGreeting { - apis: Vec, - motd: Option, -} - -#[derive(Clone, Debug, Deserialize)] -struct ServerJoined { - context: Option, -} - -#[derive(Clone, Debug, Deserialize)] -struct ServerPeerMessage { - message: String, -} - -#[derive(Clone, Debug, Deserialize)] -struct ServerSessionClosed { - reason: Option, -} - -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] -#[serde(rename_all = "kebab-case")] -enum PeerMessageType { - Ping, - Pong, - RequestSigningCertificate, - SigningCertificate, - SignRequest, - Signature, -} - -/// A peer-to-peer message. -#[derive(Clone, Debug, Deserialize, Serialize)] -struct PeerMessage { - #[serde(rename = "type")] - typ: PeerMessageType, - payload: Option, -} - -impl PeerMessage { - fn require_type(self, typ: PeerMessageType) -> Result { - if self.typ == typ { - Ok(self) - } else { - Err(RemoteSignError::ServerUnexpectedMessage(format!( - "{:?}", - self.typ - ))) - } - } - - fn as_type( - &self, - message_type: PeerMessageType, - ) -> Result { - if self.typ == message_type { - if let Some(value) = &self.payload { - Ok(serde_json::from_value(value.clone())?) - } else { - Err(RemoteSignError::ClientState( - "no payload for requested type", - )) - } - } else { - Err(RemoteSignError::ClientState( - "requested payload for wrong message type", - )) - } - } - - fn as_signing_certificate(&self) -> Result { - self.as_type::(PeerMessageType::SigningCertificate) - } - - fn as_sign_request(&self) -> Result { - self.as_type::(PeerMessageType::SignRequest) - } - - fn as_signature(&self) -> Result { - self.as_type::(PeerMessageType::Signature) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -struct PeerCertificate { - certificate: String, - #[serde(skip_serializing_if = "Vec::is_empty")] - chain: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(untagged)] -enum PeerPayload { - SigningCertificate(PeerSigningCertificate), - SignRequest(PeerSignRequest), - Signature(PeerSignature), -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -struct PeerSigningCertificate { - certificates: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -struct PeerSignRequest { - message: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -struct PeerSignature { - message: String, - signature: String, - algorithm_oid: String, -} - -const REQUIRED_ACTIONS: [&str; 4] = ["create-session", "join-session", "send-message", "goodbye"]; - -/// Represents the response from the server. -enum ServerResponse { - /// Server closed the connection. - Closed, - - /// A parsed protocol message. - Message(ServerMessage), -} - -/// A function that receives session information. -pub type SessionInfoCallback = fn(sjs_base64: &str, sjs_pem: &str) -> Result<(), RemoteSignError>; - -fn create_websocket( - req: impl IntoClientRequest, -) -> Result>, RemoteSignError> { - let config = WebSocketConfig { - max_send_queue: Some(1), - ..Default::default() - }; - - let req = req.into_client_request()?; - warn!("connecting to {}", req.uri()); - - let (ws, _) = tungstenite::client::connect_with_config(req, Some(config), 5)?; - - Ok(ws) -} - -fn wait_for_server_response( - ws: &mut WebSocket>, -) -> Result { - loop { - match ws.read_message()? { - Message::Text(text) => { - let message = serde_json::from_str::(&text)?; - debug!( - "received message; request-id: {}; type: {:?}", - message - .request_id - .as_ref() - .unwrap_or(&"(not set)".to_string()), - message.typ - ); - - return Ok(ServerResponse::Message(message)); - } - Message::Binary(_) => { - return Err(RemoteSignError::ServerUnexpectedMessage( - "binary websocket message".into(), - )) - } - // TODO return error for these? - Message::Pong(_) => {} - Message::Ping(_) => {} - Message::Frame(_) => {} - Message::Close(_) => { - return Ok(ServerResponse::Closed); - } - } - } -} - -fn wait_for_server_message( - ws: &mut WebSocket>, -) -> Result { - match wait_for_server_response(ws)? { - ServerResponse::Closed => Err(RemoteSignError::ClientState("server closed connection")), - ServerResponse::Message(m) => { - debug!( - "received server message {:?}; remaining session TTL: {}", - m.typ, - m.ttl.unwrap_or_default() - ); - Ok(m) - } - } -} - -fn wait_for_expected_server_message( - ws: &mut WebSocket>, - message_type: ServerMessageType, -) -> Result { - let res = wait_for_server_message(ws)?.into_result()?; - - if res.typ == message_type { - Ok(res) - } else { - Err(RemoteSignError::ServerUnexpectedMessage(format!( - "{:?}", - res.typ - ))) - } -} - -/// A client for the remote signing protocol that has not yet joined a session. -/// -/// Clients can perform both the initiator and signer roles. -pub struct UnjoinedSigningClient { - ws: WebSocket>, -} - -impl UnjoinedSigningClient { - fn new(req: impl IntoClientRequest) -> Result { - let ws = create_websocket(req)?; - - let mut slf = Self { ws }; - - slf.send_hello()?; - - Ok(slf) - } - - /// Create a new client in the initiator role. - pub fn new_initiator( - req: impl IntoClientRequest, - initiator: Box, - session_info_cb: Option, - ) -> Result { - let slf = Self::new(req)?; - slf.create_session_and_wait_for_signer(initiator, session_info_cb) - } - - /// Create a new client in the signer role. - pub fn new_signer( - joiner: Box, - signing_key: &dyn KeyInfoSigner, - signing_cert: CapturedX509Certificate, - certificates: Vec, - default_server_url: String, - ) -> Result { - // An error here could result in the peer hanging indefinitely because the session - // is unjoined. Ideally we'd recover from this by attempting to join with an error. - // However, we may not even be able to obtain the session ID since sometimes it is - // encrypted and the error could be from a decryption failure! So for now, just let - // the peer idle. - let join_context = joiner.join_context()?; - - let server_url = join_context - .server_url - .as_ref() - .unwrap_or(&default_server_url); - - let slf = Self::new(server_url)?; - slf.join_session(join_context, signing_key, signing_cert, certificates) - } - - /// Create a new signing session and wait for a signer to arrive. - fn create_session_and_wait_for_signer( - mut self, - initiator: Box, - session_info_cb: Option, - ) -> Result { - let session_id = initiator.session_id().to_string(); - - self.send_request( - ApiMethod::CreateSession, - Some(ClientPayload::CreateSession { - session_id: session_id.clone(), - ttl: 600, - context: initiator.session_create_context().map(base64::encode), - }), - )?; - - let sjs_base64 = initiator.session_join_string_base64()?; - let sjs_pem = initiator.session_join_string_pem()?; - - wait_for_expected_server_message(&mut self.ws, ServerMessageType::SessionCreated)?; - warn!("session successfully created on server"); - - if let Some(cb) = session_info_cb { - cb(&sjs_base64, &sjs_pem)?; - } - - let res = wait_for_expected_server_message(&mut self.ws, ServerMessageType::SessionJoined)?; - - let joined = res.as_session_joined()?; - warn!("signer joined session; deriving shared encryption key"); - - let context = if let Some(context) = joined.context { - Some(base64::decode(context)?) - } else { - None - }; - - let keys = initiator.negotiate_session(context)?; - - let mut client = PairedClient { - ws: self.ws, - session_id, - keys, - }; - - client.send_ping()?; - - let (signing_cert, signing_chain) = client.request_signing_certificate()?; - - if let Some(name) = signing_cert.subject_common_name() { - warn!("remote signer will sign with certificate: {}", name); - } - - Ok(InitiatorClient { - client: RefCell::new(client), - signing_cert, - signing_chain, - }) - } - - /// Join a signing session. - /// - /// This should be called by signers once they have the session ID to join. - pub fn join_session( - mut self, - join_context: SessionJoinContext, - signing_key: &dyn KeyInfoSigner, - signing_cert: CapturedX509Certificate, - certificates: Vec, - ) -> Result { - let session_id = join_context.session_id.clone(); - - warn!("joining session..."); - self.send_request( - ApiMethod::JoinSession, - Some(ClientPayload::JoinSession { - session_id: session_id.clone(), - context: join_context.peer_context.map(base64::encode), - }), - )?; - - wait_for_expected_server_message(&mut self.ws, ServerMessageType::SessionJoined)?; - - warn!("successfully joined signing session {}", session_id); - - let keys = join_context.peer_handshake.negotiate_session()?; - - let mut client = PairedClient { - ws: self.ws, - session_id, - keys, - }; - - warn!("verifying encrypted communications with peer"); - client.send_ping()?; - - Ok(SigningClient { - client: RefCell::new(client), - signing_key, - signing_cert, - certificates, - }) - } - - fn send_request( - &mut self, - api: ApiMethod, - payload: Option, - ) -> Result<(), RemoteSignError> { - let request_id = uuid::Uuid::new_v4().to_string(); - - let message = ClientMessage { - request_id, - api, - payload, - }; - - let body = serde_json::to_string(&message)?; - self.ws.write_message(body.into())?; - - Ok(()) - } - - fn send_hello(&mut self) -> Result<(), RemoteSignError> { - self.send_request(ApiMethod::Hello, None)?; - - let res = wait_for_expected_server_message(&mut self.ws, ServerMessageType::Greeting)?; - let greeting = res.as_greeting()?; - - if let Some(motd) = &greeting.motd { - warn!("message from remote server: {}", motd); - } - - for required in REQUIRED_ACTIONS { - if !greeting.apis.contains(&required.to_string()) { - error!("server does not support required action {}", required); - return Err(RemoteSignError::ServerIncompatible); - } - } - - Ok(()) - } -} - -/// A remote signing client that has joined a session and is ready to exchange messages. -pub struct PairedClient { - ws: WebSocket>, - session_id: String, - keys: PeerKeys, -} - -impl Drop for PairedClient { - fn drop(&mut self) { - warn!("disconnecting from relay server"); - } -} - -impl PairedClient { - fn send_request( - &mut self, - api: ApiMethod, - payload: Option, - ) -> Result<(), RemoteSignError> { - let request_id = uuid::Uuid::new_v4().to_string(); - - let message = ClientMessage { - request_id, - api, - payload, - }; - - let body = serde_json::to_string(&message)?; - self.ws.write_message(body.into())?; - - Ok(()) - } - - fn decrypt_peer_message( - &mut self, - message: &ServerPeerMessage, - ) -> Result { - let ciphertext = base64::decode(&message.message)?; - - let plaintext = self.keys.open(ciphertext)?; - - Ok(serde_json::from_slice(&plaintext)?) - } - - fn send_encrypted_message( - &mut self, - message_type: PeerMessageType, - payload: Option, - ) -> Result<(), RemoteSignError> { - let message = PeerMessage { - typ: message_type, - payload: if let Some(payload) = payload { - Some(serde_json::to_value(payload)?) - } else { - None - }, - }; - - let ciphertext = self.keys.seal(&serde_json::to_vec(&message)?)?; - - self.send_request( - ApiMethod::SendMessage, - Some(ClientPayload::SendMessage { - session_id: self.session_id.clone(), - message: base64::encode(ciphertext), - }), - )?; - - Ok(()) - } - - fn wait_for_peer_message(&mut self) -> Result, RemoteSignError> { - let res = wait_for_server_message(&mut self.ws)?.into_result()?; - - if let Ok(closed) = res.as_session_closed() { - warn!( - "signing session closed; reason: {}", - closed - .reason - .as_ref() - .unwrap_or(&"(none given)".to_string()) - ); - Ok(None) - } else { - let message = res.as_peer_message()?; - - Ok(Some(self.decrypt_peer_message(&message)?)) - } - } - - fn wait_for_server_and_peer_response(&mut self) -> Result { - let mut response = None; - - // We should get a server message acknowledging our request plus the response from - // the peer. The order they arrive in is random. - for _ in 0..2 { - let res = wait_for_server_message(&mut self.ws)?.into_result()?; - - match res.typ { - ServerMessageType::MessageSent => {} - ServerMessageType::PeerMessage => { - let message = res.as_peer_message()?; - - response = Some(self.decrypt_peer_message(&message)?); - } - m => return Err(RemoteSignError::ServerUnexpectedMessage(format!("{:?}", m))), - } - } - - if let Some(response) = response { - Ok(response) - } else { - Err(RemoteSignError::ClientState( - "failed to receive response from server or peer", - )) - } - } - - fn send_goodbye(&mut self, reason: Option) -> Result<(), RemoteSignError> { - warn!("terminating signing session on relay"); - self.send_request( - ApiMethod::Goodbye, - Some(ClientPayload::Goodbye { - session_id: self.session_id.clone(), - reason, - }), - )?; - - wait_for_server_message(&mut self.ws)?.into_result()?; - info!("relay server confirmed session termination"); - - Ok(()) - } - - fn send_ping(&mut self) -> Result<(), RemoteSignError> { - // We should get a server message acknowledging our request plus a - // ping from the peer. The order may not be reliable. - self.send_encrypted_message(PeerMessageType::Ping, None)?; - let message = self.wait_for_server_and_peer_response()?; - if !matches!(message.typ, PeerMessageType::Ping) { - return Err(RemoteSignError::ServerUnexpectedMessage( - "unexpected response to ping message".into(), - )); - } - - self.send_encrypted_message(PeerMessageType::Pong, None)?; - let message = self.wait_for_server_and_peer_response()?; - if !matches!(message.typ, PeerMessageType::Pong) { - return Err(RemoteSignError::ServerUnexpectedMessage( - "unexpected response to ping message".into(), - )); - } - - Ok(()) - } - - /// Request the signing certificate from the peer. - pub fn request_signing_certificate( - &mut self, - ) -> Result<(CapturedX509Certificate, Vec), RemoteSignError> { - warn!("requesting signing certificate info from signer"); - self.send_encrypted_message(PeerMessageType::RequestSigningCertificate, None)?; - let res = self - .wait_for_server_and_peer_response()? - .require_type(PeerMessageType::SigningCertificate)?; - - let cert = res.as_signing_certificate()?; - - if let Some(cert) = cert.certificates.get(0) { - let cert_der = base64::decode(&cert.certificate)?; - let chain_der = cert - .chain - .iter() - .map(base64::decode) - .collect::, base64::DecodeError>>()?; - - let cert = CapturedX509Certificate::from_der(cert_der)?; - let chain = chain_der - .into_iter() - .map(CapturedX509Certificate::from_der) - .collect::, X509CertificateError>>()?; - - return Ok((cert, chain)); - } - - Err(RemoteSignError::ClientState( - "did not receive any signing certificates from peer", - )) - } -} - -/// A client fulfilling the role of the initiator. -pub struct InitiatorClient { - client: RefCell, - signing_cert: CapturedX509Certificate, - signing_chain: Vec, -} - -impl InitiatorClient { - /// The X.509 certificate that will be used to sign. - pub fn signing_certificate(&self) -> &CapturedX509Certificate { - &self.signing_cert - } - - /// Additional X.509 certificates in the signing chain. - pub fn certificate_chain(&self) -> &[CapturedX509Certificate] { - &self.signing_chain - } -} - -impl Signer for InitiatorClient { - fn try_sign(&self, message: &[u8]) -> Result { - let mut client = self.client.borrow_mut(); - - warn!("sending signing request to remote signer"); - - client - .send_encrypted_message( - PeerMessageType::SignRequest, - Some(PeerPayload::SignRequest(PeerSignRequest { - message: base64::encode(message), - })), - ) - .map_err(signature::Error::from_source)?; - - let response = client - .wait_for_server_and_peer_response() - .map_err(signature::Error::from_source)? - .require_type(PeerMessageType::Signature) - .map_err(signature::Error::from_source)?; - - let peer_signature = response - .as_signature() - .map_err(signature::Error::from_source)?; - - warn!("received signature from remote signer"); - - let signature = - base64::decode(&peer_signature.signature).map_err(signature::Error::from_source)?; - let oid_der = - base64::decode(&peer_signature.algorithm_oid).map_err(signature::Error::from_source)?; - - bcder::decode::Constructed::decode(oid_der.as_ref(), Mode::Der, |cons| { - Oid::take_from(cons) - }) - .map_err(|_| { - signature::Error::from_source(RemoteSignError::Crypto( - "error parsing signature OID".into(), - )) - })?; - - // The peer could be acting maliciously (or just be buggy) and sign with a - // certificate from the initial one presented. So verify the signature we - // received is valid for the message we sent. - if let Err(e) = self.signing_cert.verify_signed_data(message, &signature) { - error!("Peer issued signature did not verify against the certificate they provided"); - error!("The peer could be acting maliciously. Or it could just be buggy."); - error!("Either way, it didn't issue a valid signature, so we're giving up."); - - return Err(signature::Error::from_source(e)); - } - - Ok(signature.into()) - } -} - -impl Sign for InitiatorClient { - fn sign(&self, message: &[u8]) -> Result<(Vec, SignatureAlgorithm), X509CertificateError> { - let algorithm = self.signature_algorithm()?; - - Ok((self.try_sign(message)?.into(), algorithm)) - } - - fn key_algorithm(&self) -> Option { - self.signing_cert.key_algorithm() - } - - fn public_key_data(&self) -> Bytes { - self.signing_cert.public_key_data() - } - - fn signature_algorithm(&self) -> Result { - if let Some(algorithm) = self.signing_cert.signature_algorithm() { - Ok(algorithm) - } else { - Err(X509CertificateError::UnknownSignatureAlgorithm(format!( - "{}", - self.signing_cert.signature_algorithm_oid() - ))) - } - } - - fn private_key_data(&self) -> Option> { - // We never have access to private keys from the remote signer. - None - } - - fn rsa_primes(&self) -> Result, Vec)>, X509CertificateError> { - // We never have access to private keys from the remote signer. - Ok(None) - } -} - -impl KeyInfoSigner for InitiatorClient {} - -impl PublicKeyPeerDecrypt for InitiatorClient { - fn decrypt(&self, _ciphertext: &[u8]) -> Result, RemoteSignError> { - Err(RemoteSignError::Crypto( - "a remote signer cannot be used to perform signing".into(), - )) - } -} - -impl PrivateKey for InitiatorClient { - fn as_key_info_signer(&self) -> &dyn KeyInfoSigner { - self - } - - fn to_public_key_peer_decrypt( - &self, - ) -> Result, AppleCodesignError> { - Err( - RemoteSignError::ClientState("cannot use remote signing initiator for decryption") - .into(), - ) - } - - fn finish(&self) -> Result<(), AppleCodesignError> { - // Tell the peer we're done so it disconnects - Ok(self - .client - .borrow_mut() - .send_goodbye(Some("signing operations completed".into()))?) - } -} - -pub struct SigningClient<'key> { - client: RefCell, - signing_key: &'key dyn KeyInfoSigner, - signing_cert: CapturedX509Certificate, - certificates: Vec, -} - -impl<'key> SigningClient<'key> { - fn send_signing_certificate( - &self, - mut client: RefMut, - ) -> Result<(), RemoteSignError> { - client.send_encrypted_message( - PeerMessageType::SigningCertificate, - Some(PeerPayload::SigningCertificate(PeerSigningCertificate { - certificates: vec![PeerCertificate { - certificate: base64::encode(self.signing_cert.encode_der()?), - chain: self - .certificates - .iter() - .map(|cert| { - let der = cert.encode_der()?; - - Ok(base64::encode(der)) - }) - .collect::, RemoteSignError>>()?, - }], - })), - )?; - - wait_for_expected_server_message(&mut client.ws, ServerMessageType::MessageSent)?; - - Ok(()) - } - - fn handle_sign_request( - &self, - mut client: RefMut, - request: PeerSignRequest, - ) -> Result<(), RemoteSignError> { - let message = base64::decode(&request.message)?; - - warn!( - "creating signature for remote message: {}", - &request.message - ); - let signature = self - .signing_key - .try_sign(&message) - .map_err(|e| RemoteSignError::Crypto(format!("when creating signature: {}", e)))?; - let algorithm = self.signing_key.signature_algorithm()?; - - let oid = Oid::from(algorithm); - let mut oid_der = vec![]; - oid.encode().write_encoded(Mode::Der, &mut oid_der)?; - - warn!("sending signature to peer"); - client.send_encrypted_message( - PeerMessageType::Signature, - Some(PeerPayload::Signature(PeerSignature { - message: base64::encode(message), - signature: base64::encode(signature), - algorithm_oid: base64::encode(oid_der), - })), - )?; - - wait_for_expected_server_message(&mut client.ws, ServerMessageType::MessageSent)?; - info!("relay acknowledged signature message received"); - - Ok(()) - } - - fn process_next_message(&self) -> Result { - let mut client = self.client.borrow_mut(); - - info!("waiting for server to send us a message..."); - let res = if let Some(res) = client.wait_for_peer_message()? { - res - } else { - return Ok(false); - }; - - match res.typ { - PeerMessageType::RequestSigningCertificate => { - self.send_signing_certificate(client)?; - } - PeerMessageType::Ping => { - client.send_encrypted_message(PeerMessageType::Pong, None)?; - wait_for_expected_server_message(&mut client.ws, ServerMessageType::MessageSent)?; - } - PeerMessageType::Pong => {} - PeerMessageType::SignRequest => { - self.handle_sign_request(client, res.as_sign_request()?)?; - } - typ => { - warn!("unprocessed message: {:?}", typ); - } - } - - Ok(true) - } - - pub fn run(self) -> Result<(), RemoteSignError> { - while self.process_next_message()? {} - - Ok(()) - } -} diff --git a/apple-codesign/src/remote_signing/session_negotiation.rs b/apple-codesign/src/remote_signing/session_negotiation.rs deleted file mode 100644 index 699db7746..000000000 --- a/apple-codesign/src/remote_signing/session_negotiation.rs +++ /dev/null @@ -1,896 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Session establishment and crypto code for remote signing protocol. -//! -//! The intent of this module / file is to isolate the code with the highest -//! sensitivity for security matters. - -use { - crate::remote_signing::RemoteSignError, - der::{Decode, Encode}, - minicbor::{encode::Write, Decode as CborDecode, Decoder, Encode as CborEncode, Encoder}, - oid_registry::OID_PKCS1_RSAENCRYPTION, - pkcs1::RsaPublicKey as RsaPublicKeyAsn1, - ring::{ - aead::{ - Aad, BoundKey, Nonce, NonceSequence, OpeningKey, SealingKey, UnboundKey, AES_128_GCM, - CHACHA20_POLY1305, NONCE_LEN, - }, - agreement::{agree_ephemeral, EphemeralPrivateKey, UnparsedPublicKey, X25519}, - hkdf::{Salt, HKDF_SHA256}, - rand::{SecureRandom, SystemRandom}, - }, - rsa::{BigUint, PaddingScheme, PublicKey, RsaPublicKey}, - scroll::{Pwrite, LE}, - spake2::{Ed25519Group, Identity, Password, Spake2}, - spki::SubjectPublicKeyInfo, - std::fmt::{Display, Formatter}, -}; - -type Result = std::result::Result; - -/// A generator of nonces that is a simple incrementing counter. -/// -/// Assumed use with ChaCha20+Poly1305. -#[derive(Default)] -struct RemoteSigningNonceSequence { - id: u32, -} - -impl NonceSequence for RemoteSigningNonceSequence { - fn advance(&mut self) -> ::std::result::Result { - let mut data = [0u8; NONCE_LEN]; - data.pwrite_with(self.id, 0, LE) - .map_err(|_| ring::error::Unspecified)?; - - self.id += 1; - - Ok(Nonce::assume_unique_for_key(data)) - } -} - -/// A nonce sequence that emits a constant value exactly once. -#[derive(Default)] -struct ConstantNonceSequence { - used: bool, -} - -impl NonceSequence for ConstantNonceSequence { - fn advance(&mut self) -> ::std::result::Result { - if self.used { - return Err(ring::error::Unspecified); - } - - self.used = true; - - Ok(Nonce::assume_unique_for_key([0x42; NONCE_LEN])) - } -} - -/// The role being assumed by a peer. -#[derive(Clone, Copy, Debug)] -pub enum Role { - /// Peer who initiated the session. - A, - /// Peer who joined the session. - B, -} - -impl Display for Role { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Self::A => "A", - Self::B => "B", - }) - } -} - -/// Derives the identifier / info value used for HKDF expansion. -fn derive_hkdf_info(role: Role, session_id: &str, extra_identifier: &[u8]) -> Vec { - role.to_string() - .as_bytes() - .iter() - .chain(std::iter::once(&b':')) - .chain(session_id.as_bytes().iter()) - .chain(std::iter::once(&b':')) - .chain(extra_identifier.iter()) - .copied() - .collect::>() -} - -pub struct PeerKeys { - sealing: SealingKey, - opening: OpeningKey, -} - -impl PeerKeys { - /// Encrypt / seal a plaintext message using AEAD. - /// - /// Receives the plaintext message to encrypt. - /// - /// Returns the encrypted ciphertext. - pub fn seal(&mut self, plaintext: &[u8]) -> Result> { - let mut output = plaintext.to_vec(); - self.sealing - .seal_in_place_append_tag(Aad::empty(), &mut output) - .map_err(|_| RemoteSignError::Crypto("AEAD sealing error".into()))?; - - Ok(output) - } - - /// Decrypt / open a ciphertext using AEAD. - /// - /// Receives the ciphertext message to decrypt. - /// - /// Returns the decrypted and verified plaintext. - pub fn open(&mut self, mut ciphertext: Vec) -> Result> { - let plaintext = self - .opening - .open_in_place(Aad::empty(), &mut ciphertext) - .map_err(|_| RemoteSignError::Crypto("failed to decrypt message".into()))?; - - Ok(plaintext.to_vec()) - } -} - -/// Derives a pair of AEAD keys from a shared encryption key. -/// -/// Returns a pair of keys. One key is used for sealing / encrypting and the -/// other for opening / decrypting. -/// -/// `role` is the role that the current peer is playing. The session initiator -/// generally uses `A` and the joiner / signer uses `B`. -/// -/// `shared_key` is a private key that is mutually derived and identical on both -/// peers. The mechanism for obtaining it varies. -/// -/// `session_id` is the server-registered session identifier. -/// -/// `extra_identifier` is an extra value to use when constructing identities for -/// HKDF extraction. -fn derive_aead_keys( - role: Role, - shared_key: Vec, - session_id: &str, - extra_identifier: &[u8], -) -> Result<( - SealingKey, - OpeningKey, -)> { - let salt = Salt::new(HKDF_SHA256, &[]); - let prk = salt.extract(&shared_key); - - let a_identifier = derive_hkdf_info(Role::A, session_id, extra_identifier); - let b_identifier = derive_hkdf_info(Role::B, session_id, extra_identifier); - - let a_info = [a_identifier.as_ref()]; - let b_info = [b_identifier.as_ref()]; - - let a_key = prk - .expand(&a_info, &CHACHA20_POLY1305) - .map_err(|_| RemoteSignError::Crypto("error performing HKDF key derivation".into()))?; - - let b_key = prk - .expand(&b_info, &CHACHA20_POLY1305) - .map_err(|_| RemoteSignError::Crypto("error performing HKDF key derivation".into()))?; - - let (sealing_key, opening_key) = match role { - Role::A => (a_key, b_key), - Role::B => (b_key, a_key), - }; - - let sealing_key = SealingKey::new(sealing_key.into(), RemoteSigningNonceSequence::default()); - let opening_key = OpeningKey::new(opening_key.into(), RemoteSigningNonceSequence::default()); - - Ok((sealing_key, opening_key)) -} - -fn encode_sjs( - scheme: &str, - payload: impl CborEncode<()>, -) -> ::std::result::Result, minicbor::encode::Error> { - let mut encoder = Encoder::new(Vec::::new()); - - { - let encoder = encoder.array(2)?; - encoder.str(scheme)?; - payload.encode(encoder, &mut ())?; - encoder.end()?; - } - - Ok(encoder.into_writer()) -} - -/// Common behaviors for a session join string. -/// -/// Implementations must also implement [Encode], which will emit the CBOR -/// encoding of the instance to an encoder. -pub trait SessionJoinString<'de>: CborDecode<'de, ()> + CborEncode<()> { - /// The scheme / name for this SJS implementation. - /// - /// This is advertised as the first component in the encoded SJS. - fn scheme() -> &'static str; - - /// Obtain the raw bytes constituting the session join string. - fn to_bytes(&self) -> Result> { - encode_sjs(Self::scheme(), &self) - .map_err(|e| RemoteSignError::SessionJoinString(format!("CBOR encoding error: {}", e))) - } -} - -struct PublicKeySessionJoinString { - aes_ciphertext: Vec, - public_key: Vec, - message_ciphertext: Vec, -} - -impl<'de, C> CborDecode<'de, C> for PublicKeySessionJoinString { - fn decode( - d: &mut Decoder<'de>, - _ctx: &mut C, - ) -> std::result::Result { - if !matches!(d.array()?, Some(3)) { - return Err(minicbor::decode::Error::message( - "not an array of 3 elements", - )); - } - - let aes_ciphertext = d.bytes()?.to_vec(); - let public_key = d.bytes()?.to_vec(); - let message_ciphertext = d.bytes()?.to_vec(); - - Ok(Self { - aes_ciphertext, - public_key, - message_ciphertext, - }) - } -} - -impl CborEncode for PublicKeySessionJoinString { - fn encode( - &self, - e: &mut Encoder, - _ctx: &mut C, - ) -> ::std::result::Result<(), minicbor::encode::Error> { - e.array(3)?; - e.bytes(&self.aes_ciphertext)?; - e.bytes(&self.public_key)?; - e.bytes(&self.message_ciphertext)?; - e.end()?; - - Ok(()) - } -} - -impl SessionJoinString<'static> for PublicKeySessionJoinString { - fn scheme() -> &'static str { - "publickey0" - } -} - -struct SharedSecretSessionJoinString { - session_id: String, - extra_identifier: Vec, - role_a_init_message: Vec, -} - -impl<'de, C> CborDecode<'de, C> for SharedSecretSessionJoinString { - fn decode( - d: &mut Decoder<'de>, - _ctx: &mut C, - ) -> std::result::Result { - if !matches!(d.array()?, Some(3)) { - return Err(minicbor::decode::Error::message( - "not an array of 3 elements", - )); - } - - let session_id = d.str()?.to_string(); - let extra_identifier = d.bytes()?.to_vec(); - let role_a_init_message = d.bytes()?.to_vec(); - - Ok(Self { - session_id, - extra_identifier, - role_a_init_message, - }) - } -} - -impl CborEncode for SharedSecretSessionJoinString { - fn encode( - &self, - e: &mut Encoder, - _ctx: &mut C, - ) -> ::std::result::Result<(), minicbor::encode::Error> { - e.array(3)?; - e.str(&self.session_id)?; - e.bytes(&self.extra_identifier)?; - e.bytes(&self.role_a_init_message)?; - e.end()?; - - Ok(()) - } -} - -impl SessionJoinString<'static> for SharedSecretSessionJoinString { - fn scheme() -> &'static str { - "sharedsecret0" - } -} - -/// A peer that initiates a remote signing session. -pub trait SessionInitiatePeer { - /// Obtain the session ID to create / use. - fn session_id(&self) -> &str; - - /// Obtain additional session context to store with the server. - /// - /// This context will be sent to the peer when it joins. - fn session_create_context(&self) -> Option>; - - /// Obtain the raw bytes constituting the session join string. - fn session_join_string_bytes(&self) -> Result>; - - /// Obtain the base 64 encoded session join string. - fn session_join_string_base64(&self) -> Result { - Ok(base64::encode_config( - self.session_join_string_bytes()?, - base64::URL_SAFE_NO_PAD, - )) - } - - /// Obtain the PEM encoded session join string. - fn session_join_string_pem(&self) -> Result { - Ok(pem::encode(&pem::Pem { - tag: "SESSION JOIN STRING".to_string(), - contents: self.session_join_string_bytes()?, - })) - } - - /// Finalize a peer joined session using optional context provided by the peer. - /// - /// Yields encryption keys for this peer. - fn negotiate_session(self: Box, peer_context: Option>) -> Result; -} - -pub enum SessionJoinState { - /// A generic shared secret value. - SharedSecret(Vec), - - /// An entity capable of decrypting messages encrypted by the peer. - PublicKeyDecrypt(Box), -} - -/// A peer that joins sessions in a state before it has spoken to the server. -pub trait SessionJoinPeerPreJoin { - /// Register additional state with the peer. - /// - /// This is used as a generic way to import implementation-specific state that - /// enables the peer join to complete. - fn register_state(&mut self, state: SessionJoinState) -> Result<()>; - - /// Obtain information needed to join to a session. - /// - /// Consumes self because joining should be a one-time operation. - fn join_context(self: Box) -> Result; -} - -pub trait SessionJoinPeerHandshake { - /// Finalize a peer joining session. - /// - /// Yields encryption keys for this peer. - fn negotiate_session(self: Box) -> Result; -} - -/// Holds data needs to enable a joining peer to join a session. -pub struct SessionJoinContext { - /// URL of server to join. - /// - /// If not set, the client default URL is used. - pub server_url: Option, - - /// The session ID to join. - pub session_id: String, - - /// Additional data to relay to the peer to enable it to finalize the session. - pub peer_context: Option>, - - /// Object that will finalize the peer handshake and derive encryption keys. - pub peer_handshake: Box, -} - -#[derive(CborDecode, CborEncode)] -#[cbor(array)] -struct PublicKeySecretMessage { - #[n(0)] - server_url: Option, - - #[n(1)] - session_id: String, - - #[n(2)] - challenge: Vec, - - #[n(3)] - agreement_public: Vec, -} - -pub struct PublicKeyInitiator { - session_id: String, - extra_identifier: Vec, - sjs: PublicKeySessionJoinString, - agreement_private: EphemeralPrivateKey, -} - -impl SessionInitiatePeer for PublicKeyInitiator { - fn session_id(&self) -> &str { - &self.session_id - } - - fn session_create_context(&self) -> Option> { - None - } - - fn session_join_string_bytes(&self) -> Result> { - self.sjs.to_bytes() - } - - fn negotiate_session(self: Box, peer_context: Option>) -> Result { - let public_key = peer_context.ok_or_else(|| { - RemoteSignError::Crypto( - "missing peer public key context in session join message".into(), - ) - })?; - - let public_key = UnparsedPublicKey::new(&X25519, public_key); - - let (sealing, opening) = agree_ephemeral( - self.agreement_private, - &public_key, - RemoteSignError::Crypto("error deriving agreement key".into()), - |agreement_key| { - derive_aead_keys( - Role::A, - agreement_key.to_vec(), - &self.session_id, - &self.extra_identifier, - ) - }, - ) - .map_err(|_| { - RemoteSignError::Crypto("error deriving AEAD keys from agreement key".into()) - })?; - - Ok(PeerKeys { sealing, opening }) - } -} - -impl PublicKeyInitiator { - /// Create a new initiator using public key agreement. - pub fn new(peer_public_key: impl AsRef<[u8]>, server_url: Option) -> Result { - let spki = SubjectPublicKeyInfo::from_der(peer_public_key.as_ref()) - .map_err(|e| RemoteSignError::Crypto(format!("when parsing SPKI data: {}", e)))?; - - let session_id = uuid::Uuid::new_v4().to_string(); - - let rng = SystemRandom::new(); - - let mut challenge = [0u8; 32]; - rng.fill(&mut challenge) - .map_err(|_| RemoteSignError::Crypto("failed to generate random data".into()))?; - - let mut aes_key_data = [0u8; 16]; - rng.fill(&mut aes_key_data) - .map_err(|_| RemoteSignError::Crypto("failed to generate random data".into()))?; - - let agreement_private = EphemeralPrivateKey::generate(&X25519, &rng).map_err(|_| { - RemoteSignError::Crypto("failed to generate ephemeral agreement key".into()) - })?; - - let agreement_public = agreement_private.compute_public_key().map_err(|_| { - RemoteSignError::Crypto( - "failed to derive public key from ephemeral agreement key".into(), - ) - })?; - - let peer_message = PublicKeySecretMessage { - server_url, - session_id: session_id.clone(), - challenge: challenge.as_ref().to_vec(), - agreement_public: agreement_public.as_ref().to_vec(), - }; - - // The unique AES key is used to encrypt the main CBOR message. - let mut message_ciphertext = minicbor::to_vec(peer_message) - .map_err(|e| RemoteSignError::Crypto(format!("CBOR encode error: {}", e)))?; - let aes_key = UnboundKey::new(&AES_128_GCM, &aes_key_data).map_err(|_| { - RemoteSignError::Crypto("failed to load AES encryption key into ring".into()) - })?; - let mut sealing_key = SealingKey::new(aes_key, ConstantNonceSequence::default()); - sealing_key - .seal_in_place_append_tag(Aad::empty(), &mut message_ciphertext) - .map_err(|_| RemoteSignError::Crypto("failed to AES encrypt message to peer".into()))?; - - // The AES encrypting key is encrypted using asymmetric encryption. - - let aes_ciphertext = match spki.algorithm.oid.as_ref() { - x if x == OID_PKCS1_RSAENCRYPTION.as_bytes() => { - let public_key = - RsaPublicKeyAsn1::from_der(spki.subject_public_key).map_err(|e| { - RemoteSignError::Crypto(format!("when parsing RSA public key: {}", e)) - })?; - - let n = BigUint::from_bytes_be(public_key.modulus.as_bytes()); - let e = BigUint::from_bytes_be(public_key.public_exponent.as_bytes()); - - let rsa_public = RsaPublicKey::new(n, e).map_err(|e| { - RemoteSignError::Crypto(format!("when constructing RSA public key: {}", e)) - })?; - - let padding = PaddingScheme::new_oaep::(); - - rsa_public - .encrypt(&mut rand::thread_rng(), padding, &aes_key_data) - .map_err(|e| { - RemoteSignError::Crypto(format!("RSA public key encryption error: {}", e)) - })? - } - _ => { - return Err(RemoteSignError::Crypto(format!( - "do not know how to encrypt for algorithm {}", - spki.algorithm.oid - ))); - } - }; - - let public_key = spki - .to_vec() - .map_err(|e| RemoteSignError::Crypto(format!("when encoding SPKI to DER: {}", e)))?; - - let sjs = PublicKeySessionJoinString { - aes_ciphertext, - public_key, - message_ciphertext, - }; - - Ok(Self { - session_id, - extra_identifier: challenge.as_ref().to_vec(), - sjs, - agreement_private, - }) - } -} - -/// Describes a type that is capable of decrypting messages used during public key negotiation. -pub trait PublicKeyPeerDecrypt { - /// Decrypt an encrypted message. - fn decrypt(&self, ciphertext: &[u8]) -> Result>; -} - -/// A joining peer using public key encryption. -struct PublicKeyPeerPreJoined { - sjs: PublicKeySessionJoinString, - - decrypter: Option>, -} - -impl SessionJoinPeerPreJoin for PublicKeyPeerPreJoined { - fn register_state(&mut self, state: SessionJoinState) -> Result<()> { - match state { - SessionJoinState::PublicKeyDecrypt(decrypt) => { - self.decrypter = Some(decrypt); - Ok(()) - } - SessionJoinState::SharedSecret(_) => Ok(()), - } - } - - fn join_context(self: Box) -> Result { - let decrypter = self - .decrypter - .ok_or_else(|| RemoteSignError::Crypto("decryption key not registered".into()))?; - - let aes_key = decrypter.decrypt(&self.sjs.aes_ciphertext)?; - let aes_key = UnboundKey::new(&AES_128_GCM, &aes_key).map_err(|_| { - RemoteSignError::Crypto("failed to construct AES key from key data".into()) - })?; - let mut opening_key = OpeningKey::new(aes_key, ConstantNonceSequence::default()); - - let mut cbor_message = self.sjs.message_ciphertext.clone(); - let cbor_plaintext = opening_key - .open_in_place(Aad::empty(), &mut cbor_message) - .map_err(|_| { - RemoteSignError::Crypto("failed to decrypt using shared AES key".into()) - })?; - - // The plaintext is a CBOR encoded message. - let message = minicbor::decode::(cbor_plaintext) - .map_err(|e| RemoteSignError::Crypto(format!("CBOR decode error: {}", e)))?; - - let agreement_private = EphemeralPrivateKey::generate(&X25519, &SystemRandom::new()) - .map_err(|_| { - RemoteSignError::Crypto("failed to generate ephemeral agreement key".into()) - })?; - let agreement_public = agreement_private.compute_public_key().map_err(|_| { - RemoteSignError::Crypto( - "failed to derive public key from ephemeral agreement key".into(), - ) - })?; - - let peer_handshake = Box::new(PublicKeyHandshakePeer { - session_id: message.session_id.clone(), - extra_identifier: message.challenge, - agreement_private, - agreement_public: message.agreement_public, - }); - - Ok(SessionJoinContext { - server_url: message.server_url, - session_id: message.session_id, - peer_context: Some(agreement_public.as_ref().to_vec()), - peer_handshake, - }) - } -} - -impl PublicKeyPeerPreJoined { - fn new(sjs: PublicKeySessionJoinString) -> Result { - Ok(Self { - sjs, - decrypter: None, - }) - } -} - -pub struct PublicKeyHandshakePeer { - session_id: String, - extra_identifier: Vec, - agreement_private: EphemeralPrivateKey, - agreement_public: Vec, -} - -impl SessionJoinPeerHandshake for PublicKeyHandshakePeer { - fn negotiate_session(self: Box) -> Result { - let peer_public_key = UnparsedPublicKey::new(&X25519, &self.agreement_public); - - let (sealing, opening) = agree_ephemeral( - self.agreement_private, - &peer_public_key, - RemoteSignError::Crypto("error deriving agreement key".into()), - |agreement_key| { - derive_aead_keys( - Role::B, - agreement_key.to_vec(), - &self.session_id, - &self.extra_identifier, - ) - }, - ) - .map_err(|_| { - RemoteSignError::Crypto("error deriving AEAD keys from agreement key".into()) - })?; - - Ok(PeerKeys { sealing, opening }) - } -} - -fn spake_identity(role: Role, session_id: &str, extra_identifier: &[u8]) -> Identity { - Identity::new(&derive_hkdf_info(role, session_id, extra_identifier)) -} - -pub struct SharedSecretInitiator { - sjs: SharedSecretSessionJoinString, - spake: Spake2, -} - -impl SessionInitiatePeer for SharedSecretInitiator { - fn session_id(&self) -> &str { - &self.sjs.session_id - } - - fn session_create_context(&self) -> Option> { - None - } - - fn session_join_string_bytes(&self) -> Result> { - self.sjs.to_bytes() - } - - fn negotiate_session(self: Box, peer_context: Option>) -> Result { - let spake_b = peer_context.ok_or_else(|| { - RemoteSignError::Crypto( - "missing SPAKE2 initialization context in session join message".into(), - ) - })?; - - let shared_key = self.spake.finish(&spake_b).map_err(|e| { - RemoteSignError::Crypto(format!("error finishing SPAKE2 key negotiation: {}", e)) - })?; - - let (sealing, opening) = derive_aead_keys( - Role::A, - shared_key, - &self.sjs.session_id, - &self.sjs.extra_identifier, - )?; - - Ok(PeerKeys { sealing, opening }) - } -} - -impl SharedSecretInitiator { - pub fn new(shared_secret: Vec) -> Result { - let session_id = uuid::Uuid::new_v4().to_string(); - - let rng = SystemRandom::new(); - let mut extra_identifier = [0u8; 16]; - rng.fill(&mut extra_identifier) - .map_err(|_| RemoteSignError::Crypto("unable to generate random value".into()))?; - - let (spake, role_a_init_message) = Spake2::::start_a( - &Password::new(shared_secret), - &spake_identity(Role::A, &session_id, &extra_identifier), - &spake_identity(Role::B, &session_id, &extra_identifier), - ); - - Ok(Self { - sjs: SharedSecretSessionJoinString { - session_id, - extra_identifier: extra_identifier.as_ref().to_vec(), - role_a_init_message, - }, - spake, - }) - } -} - -/// A joining peer using shared secrets. -struct SharedSecretPeerPreJoined { - sjs: SharedSecretSessionJoinString, - shared_secret: Option>, -} - -impl SessionJoinPeerPreJoin for SharedSecretPeerPreJoined { - fn register_state(&mut self, state: SessionJoinState) -> Result<()> { - match state { - SessionJoinState::SharedSecret(secret) => { - self.shared_secret = Some(secret); - Ok(()) - } - SessionJoinState::PublicKeyDecrypt(_) => Ok(()), - } - } - - fn join_context(self: Box) -> Result { - let shared_secret = self - .shared_secret - .as_ref() - .ok_or_else(|| RemoteSignError::Crypto("shared secret not defined".into()))?; - - let (spake, init_message) = Spake2::::start_b( - &Password::new(shared_secret), - &spake_identity(Role::A, &self.sjs.session_id, &self.sjs.extra_identifier), - &spake_identity(Role::B, &self.sjs.session_id, &self.sjs.extra_identifier), - ); - - let peer_handshake = Box::new(SharedSecretHandshakePeer { - session_id: self.sjs.session_id.clone(), - extra_identifier: self.sjs.extra_identifier, - role_a_init_message: self.sjs.role_a_init_message, - spake, - }); - - Ok(SessionJoinContext { - // TODO set this field if not the default. - server_url: None, - session_id: self.sjs.session_id, - peer_context: Some(init_message), - peer_handshake, - }) - } -} - -impl SharedSecretPeerPreJoined { - fn new(sjs: SharedSecretSessionJoinString) -> Result { - Ok(Self { - sjs, - shared_secret: None, - }) - } -} - -pub struct SharedSecretHandshakePeer { - session_id: String, - extra_identifier: Vec, - role_a_init_message: Vec, - spake: Spake2, -} - -impl SessionJoinPeerHandshake for SharedSecretHandshakePeer { - fn negotiate_session(self: Box) -> Result { - let shared_key = self.spake.finish(&self.role_a_init_message).map_err(|e| { - RemoteSignError::Crypto(format!("error finishing SPAKE2 key negotiation: {}", e)) - })?; - - let (sealing, opening) = derive_aead_keys( - Role::B, - shared_key, - &self.session_id, - &self.extra_identifier, - )?; - - Ok(PeerKeys { sealing, opening }) - } -} - -pub fn create_session_joiner( - session_join_string: impl ToString, -) -> Result> { - let input = session_join_string.to_string(); - - let trimmed = input.trim(); - - // Multiline is assumed to be PEM. - let sjs = if trimmed.contains('\n') { - let no_comments = trimmed - .lines() - .filter(|line| !line.starts_with('#')) - .collect::>() - .join("\n"); - - let doc = pem::parse(no_comments.as_bytes())?; - - if doc.tag == "SESSION JOIN STRING" { - doc.contents - } else { - return Err(RemoteSignError::SessionJoinString( - "PEM does not define a SESSION JOIN STRING".into(), - )); - } - } else { - base64::decode_config(trimmed.as_bytes(), base64::URL_SAFE_NO_PAD)? - }; - - let mut decoder = Decoder::new(&sjs); - if !matches!( - decoder.array().map_err(|_| { - RemoteSignError::SessionJoinString("decode error: not a CBOR array".into()) - })?, - Some(2) - ) { - return Err(RemoteSignError::SessionJoinString( - "decode error: not a CBOR array with 2 elements".into(), - )); - } - - let scheme = decoder - .str() - .map_err(|_| RemoteSignError::SessionJoinString("failed to decode scheme name".into()))?; - - match scheme { - _ if scheme == PublicKeySessionJoinString::scheme() => { - let sjs = PublicKeySessionJoinString::decode(&mut decoder, &mut ()).map_err(|e| { - RemoteSignError::SessionJoinString(format!("error decoding payload: {}", e)) - })?; - - Ok(Box::new(PublicKeyPeerPreJoined::new(sjs)?) as Box) - } - _ if scheme == SharedSecretSessionJoinString::scheme() => { - let sjs = - SharedSecretSessionJoinString::decode(&mut decoder, &mut ()).map_err(|e| { - RemoteSignError::SessionJoinString(format!("error decoding payload: {}", e)) - })?; - - Ok(Box::new(SharedSecretPeerPreJoined::new(sjs)?) as Box) - } - _ => Err(RemoteSignError::SessionJoinString(format!( - "unknown scheme: {}", - scheme - ))), - } -} diff --git a/apple-codesign/src/signing.rs b/apple-codesign/src/signing.rs deleted file mode 100644 index f3e901ed6..000000000 --- a/apple-codesign/src/signing.rs +++ /dev/null @@ -1,229 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! High level signing primitives. - -use { - crate::{ - bundle_signing::BundleSigner, - dmg::DmgSigner, - error::AppleCodesignError, - macho_signing::{write_macho_file, MachOSigner}, - reader::PathType, - signing_settings::{SettingsScope, SigningSettings}, - }, - apple_xar::{reader::XarReader, signing::XarSigner}, - log::{info, warn}, - std::{fs::File, path::Path}, -}; - -/// An entity for performing signing that is able to handle all supported target types. -pub struct UnifiedSigner<'key> { - settings: SigningSettings<'key>, -} - -impl<'key> UnifiedSigner<'key> { - /// Construct a new instance bound to a [SigningSettings]. - pub fn new(settings: SigningSettings<'key>) -> Self { - Self { settings } - } - - /// Signs `input_path` and writes the signed output to `output_path`. - pub fn sign_path( - &self, - input_path: impl AsRef, - output_path: impl AsRef, - ) -> Result<(), AppleCodesignError> { - let input_path = input_path.as_ref(); - - match PathType::from_path(input_path)? { - PathType::Bundle => self.sign_bundle(input_path, output_path), - PathType::Dmg => self.sign_dmg(input_path, output_path), - PathType::MachO => self.sign_macho(input_path, output_path), - PathType::Xar => self.sign_xar(input_path, output_path), - PathType::Other => Err(AppleCodesignError::UnrecognizedPathType), - } - } - - /// Sign a filesystem path in place. - /// - /// This is just a convenience wrapper for [Self::sign_path()] with the same path passed - /// to both the input and output path. - pub fn sign_path_in_place(&self, path: impl AsRef) -> Result<(), AppleCodesignError> { - let path = path.as_ref(); - - self.sign_path(path, path) - } - - /// Sign a Mach-O binary. - pub fn sign_macho( - &self, - input_path: impl AsRef, - output_path: impl AsRef, - ) -> Result<(), AppleCodesignError> { - let input_path = input_path.as_ref(); - let output_path = output_path.as_ref(); - - warn!("signing {} as a Mach-O binary", input_path.display()); - let macho_data = std::fs::read(input_path)?; - - let mut settings = self.settings.clone(); - - 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(); - - warn!("setting binary identifier to {}", identifier); - settings.set_binary_identifier(SettingsScope::Main, identifier); - } - - warn!("parsing Mach-O"); - let signer = MachOSigner::new(&macho_data)?; - - let mut macho_data = vec![]; - signer.write_signed_binary(&settings, &mut macho_data)?; - warn!("writing Mach-O to {}", output_path.display()); - write_macho_file(input_path, output_path, &macho_data)?; - - Ok(()) - } - - /// Sign a `.dmg` file. - pub fn sign_dmg( - &self, - input_path: impl AsRef, - output_path: impl AsRef, - ) -> Result<(), AppleCodesignError> { - let input_path = input_path.as_ref(); - let output_path = output_path.as_ref(); - - warn!("signing {} as a DMG", input_path.display()); - - // There must be a binary identifier on the DMG. So try to derive one - // from the filename if one isn't present in the settings. - let mut settings = self.settings.clone(); - - if settings.binary_identifier(SettingsScope::Main).is_none() { - let file_name = input_path - .file_stem() - .ok_or_else(|| { - AppleCodesignError::CliGeneralError("unable to resolve file name of DMG".into()) - })? - .to_string_lossy(); - - warn!( - "setting binary identifier to {} (derived from file name)", - file_name - ); - settings.set_binary_identifier(SettingsScope::Main, file_name); - } - - // The DMG signer signs in place because it needs a `File` handle. So if - // the output path is different, copy the DMG first. - - // This is not robust same file detection. - if input_path != output_path { - info!( - "copying {} to {} in preparation for signing", - input_path.display(), - output_path.display() - ); - if let Some(parent) = output_path.parent() { - std::fs::create_dir_all(parent)?; - } - - std::fs::copy(input_path, output_path)?; - } - - let signer = DmgSigner::default(); - let mut fh = std::fs::File::options() - .read(true) - .write(true) - .open(output_path)?; - signer.sign_file(&settings, &mut fh)?; - - Ok(()) - } - - /// Sign a bundle. - pub fn sign_bundle( - &self, - input_path: impl AsRef, - output_path: impl AsRef, - ) -> Result<(), AppleCodesignError> { - let input_path = input_path.as_ref(); - warn!("signing bundle at {}", input_path.display()); - - let signer = BundleSigner::new_from_path(input_path)?; - signer.write_signed_bundle(output_path, &self.settings)?; - - Ok(()) - } - - pub fn sign_xar( - &self, - input_path: impl AsRef, - output_path: impl AsRef, - ) -> Result<(), AppleCodesignError> { - let input_path = input_path.as_ref(); - let output_path = output_path.as_ref(); - - // The XAR can get corrupted if we sign into place. So we always go through a temporary - // file. We could potentially avoid the overhead if we're not signing in place... - - let output_path_temp = - output_path.with_file_name(if let Some(file_name) = output_path.file_name() { - file_name.to_string_lossy().to_string() + ".tmp" - } else { - "xar.tmp".to_string() - }); - - warn!( - "signing XAR pkg installer at {} to {}", - input_path.display(), - output_path_temp.display() - ); - - let (signing_key, signing_cert) = self - .settings - .signing_key() - .ok_or(AppleCodesignError::XarNoAdhoc)?; - - { - let reader = XarReader::new(File::open(input_path)?)?; - let mut signer = XarSigner::new(reader); - - let mut fh = File::create(&output_path_temp)?; - signer.sign( - &mut fh, - signing_key, - signing_cert, - self.settings.time_stamp_url(), - self.settings.certificate_chain().iter().cloned(), - )?; - } - - if output_path.exists() { - warn!("removing existing {}", output_path.display()); - std::fs::remove_file(&output_path)?; - } - - warn!( - "renaming {} -> {}", - output_path_temp.display(), - output_path.display() - ); - std::fs::rename(&output_path_temp, &output_path)?; - - Ok(()) - } -} diff --git a/apple-codesign/src/signing_settings.rs b/apple-codesign/src/signing_settings.rs deleted file mode 100644 index 76c2feef2..000000000 --- a/apple-codesign/src/signing_settings.rs +++ /dev/null @@ -1,1246 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Code signing settings. - -use { - crate::{ - certificate::AppleCertificate, - code_directory::CodeSignatureFlags, - code_requirement::CodeRequirementExpression, - embedded_signature::{Blob, DigestType, RequirementBlob}, - error::AppleCodesignError, - macho::{parse_version_nibbles, MachFile}, - }, - glob::Pattern, - goblin::mach::cputype::{ - CpuType, CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_ARM64_32, CPU_TYPE_X86_64, - }, - log::info, - reqwest::{IntoUrl, Url}, - std::{ - collections::{BTreeMap, BTreeSet}, - fmt::Formatter, - }, - x509_certificate::{CapturedX509Certificate, KeyInfoSigner}, -}; - -/// Denotes the scope for a setting. -/// -/// Settings have an associated scope defined by this type. This allows settings -/// to apply to exactly what you want them to apply to. -/// -/// Scopes can be converted from a string representation. The following syntax is -/// recognized: -/// -/// * `@main` - Maps to [SettingsScope::Main] -/// * `@` - e.g. `@0`. Maps to [SettingsScope::MultiArchIndex].Index -/// * `@[cpu_type=]` - e.g. `@[cpu_type=7]`. Maps to [SettingsScope::MultiArchCpuType]. -/// * `@[cpu_type=]` - e.g. `@[cpu_type=x86_64]`. Maps to [SettingsScope::MultiArchCpuType] -/// for recognized string values (see below). -/// * `` - e.g. `path/to/file`. Maps to [SettingsScope::Path]. -/// * `@` - e.g. `path/to/file@0`. Maps to [SettingsScope::PathMultiArchIndex]. -/// * `@[cpu_type=]` - e.g. `path/to/file@[cpu_type=7]`. Maps to -/// [SettingsScope::PathMultiArchCpuType]. -/// * `@[cpu_type=]` - e.g. `path/to/file@[cpu_type=arm64]`. Maps to -/// [SettingsScope::PathMultiArchCpuType] for recognized string values (see below). -/// -/// # Recognized cpu_type String Values -/// -/// The following `cpu_type=` string values are recognized: -/// -/// * `arm` -> [CPU_TYPE_ARM] -/// * `arm64` -> [CPU_TYPE_ARM64] -/// * `arm64_32` -> [CPU_TYPE_ARM64_32] -/// * `x86_64` -> [CPU_TYPE_X86_64] -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub enum SettingsScope { - // The order of the variants is important. Instance cloning iterates keys in - // sorted order and last write wins. So the order here should be from widest to - // most granular. - /// The main entity being signed. - /// - /// Can be a Mach-O file, a bundle, or any other primitive this crate - /// supports signing. - /// - /// When signing a bundle or any primitive with nested elements (such as a - /// fat/universal Mach-O binary), settings can propagate to nested elements. - Main, - - /// Filesystem path. - /// - /// Can refer to a Mach-O file, a nested bundle, or any other filesystem - /// based primitive that can be traversed into when performing nested signing. - /// - /// The string value refers to the filesystem relative path of the entity - /// relative to the main entity being signed. - Path(String), - - /// A single Mach-O binary within a fat/universal Mach-O binary. - /// - /// The binary to operate on is defined by its 0-based index within the - /// fat/universal Mach-O container. - MultiArchIndex(usize), - - /// A single Mach-O binary within a fat/universal Mach-O binary. - /// - /// The binary to operate on is defined by its CPU architecture. - MultiArchCpuType(CpuType), - - /// Combination of [SettingsScope::Path] and [SettingsScope::MultiArchIndex]. - /// - /// This refers to a single Mach-O binary within a fat/universal binary at a - /// given relative path. - PathMultiArchIndex(String, usize), - - /// Combination of [SettingsScope::Path] and [SettingsScope::MultiArchCpuType]. - /// - /// This refers to a single Mach-O binary within a fat/universal binary at a - /// given relative path. - 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, Option), AppleCodesignError> { - match at_expr.parse::() { - 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::>(); - - 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::() { - 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 =", - v - ))) - } - } else { - Err(AppleCodesignError::ParseSettingsScope(format!( - "in '{}', @ expression not recognized", - at_expr - ))) - } - } - } - } -} - -impl AsRef for SettingsScope { - fn as_ref(&self) -> &SettingsScope { - self - } -} - -impl TryFrom<&str> for SettingsScope { - type Error = AppleCodesignError; - - fn try_from(s: &str) -> Result { - 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 { - // Looks like a path. - let parts = s.rsplitn(2, '@').collect::>(); - - match parts.len() { - 1 => Ok(Self::Path(s.to_string())), - 2 => { - // Parts are reversed since splitting at end. - 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"), - } - } - } -} - -/// Describes how to derive designated requirements during signing. -#[derive(Clone, Debug)] -pub enum DesignatedRequirementMode { - /// Automatically attempt to derive an appropriate expression given the - /// code signing certificate and entity being signed. - Auto, - - /// Provide an explicit designated requirement. - Explicit(Vec>), -} - -/// Represents code signing settings. -/// -/// This type holds settings related to a single logical signing operation. -/// Some settings (such as the signing key-pair are global). Other settings -/// (such as the entitlements or designated requirement) can be applied on a -/// more granular, scoped basis. The scoping of these lower-level settings is -/// controlled via [SettingsScope]. If a setting is specified with a scope, it -/// only applies to that scope. See that type's documentation for more. -/// -/// An instance of this type is bound to a signing operation. When the -/// signing operation traverses into nested primitives (e.g. when traversing -/// into the individual Mach-O binaries in a fat/universal binary or when -/// traversing into nested bundles or non-main binaries within a bundle), a -/// new instance of this type is transparently constructed by merging global -/// settings with settings for the target scope. This allows granular control -/// over which signing settings apply to which entity and enables a signing -/// operation over a complex primitive to be configured/performed via a single -/// [SigningSettings] and signing operation. -#[derive(Clone, Default)] -pub struct SigningSettings<'key> { - // Global settings. - signing_key: Option<(&'key dyn KeyInfoSigner, CapturedX509Certificate)>, - certificates: Vec, - time_stamp_url: Option, - digest_type: DigestType, - path_exclusion_patterns: Vec, - - // Scope-specific settings. - // These are BTreeMap so when we filter the keys, keys with higher precedence come - // last and last write wins. - team_id: BTreeMap, - identifiers: BTreeMap, - entitlements: BTreeMap, - designated_requirement: BTreeMap, - code_signature_flags: BTreeMap, - runtime_version: BTreeMap, - info_plist_data: BTreeMap>, - code_resources_data: BTreeMap>, - extra_digests: BTreeMap>, -} - -impl<'key> SigningSettings<'key> { - /// Obtain the digest type to use. - pub fn digest_type(&self) -> &DigestType { - &self.digest_type - } - - /// Set the content digest to use. - /// - /// The default is SHA-256. Changing this to SHA-1 can weaken security of digital - /// signatures and may prevent the binary from running in environments that enforce - /// more modern signatures. - pub fn set_digest_type(&mut self, digest_type: DigestType) { - self.digest_type = digest_type; - } - - /// Obtain the signing key to use. - pub fn signing_key(&self) -> Option<(&'key dyn KeyInfoSigner, &CapturedX509Certificate)> { - self.signing_key.as_ref().map(|(key, cert)| (*key, cert)) - } - - /// Set the signing key-pair for producing a cryptographic signature over code. - /// - /// If this is not called, signing will lack a cryptographic signature and will only - /// contain digests of content. This is known as "ad-hoc" mode. Binaries lacking a - /// cryptographic signature or signed without a key-pair issued/signed by Apple may - /// not run in all environments. - pub fn set_signing_key( - &mut self, - private: &'key dyn KeyInfoSigner, - public: CapturedX509Certificate, - ) { - self.signing_key = Some((private, public)); - } - - /// Obtain the certificate chain. - pub fn certificate_chain(&self) -> &[CapturedX509Certificate] { - &self.certificates - } - - /// Attempt to chain Apple CA certificates from a loaded Apple signed signing key. - /// - /// If you are calling `set_signing_key()`, you probably want to call this immediately - /// afterwards, as it will automatically register Apple CA certificates if you are - /// using an Apple signed code signing certificate. - pub fn chain_apple_certificates(&mut self) -> Option> { - if let Some((_, cert)) = &self.signing_key { - if let Some(chain) = cert.apple_root_certificate_chain() { - // The chain starts with self. - let chain = chain.into_iter().skip(1).collect::>(); - self.certificates.extend(chain.clone()); - Some(chain) - } else { - None - } - } else { - None - } - } - - /// Add a parsed certificate to the signing certificate chain. - /// - /// When producing a cryptographic signature (see [SigningSettings::set_signing_key]), - /// information about the signing key-pair is included in the signature. The signing - /// key's public certificate is always included. This function can be used to define - /// additional X.509 public certificates to include. Typically, the signing chain - /// of the signing key-pair up until the root Certificate Authority (CA) is added - /// so clients have access to the full certificate chain for validation purposes. - /// - /// This setting has no effect if [SigningSettings::set_signing_key] is not called. - pub fn chain_certificate(&mut self, cert: CapturedX509Certificate) { - self.certificates.push(cert); - } - - /// Add a DER encoded X.509 public certificate to the signing certificate chain. - /// - /// This is like [Self::chain_certificate] except the certificate data is provided in - /// its binary, DER encoded form. - pub fn chain_certificate_der( - &mut self, - data: impl AsRef<[u8]>, - ) -> Result<(), AppleCodesignError> { - self.chain_certificate(CapturedX509Certificate::from_der(data.as_ref())?); - - Ok(()) - } - - /// Add a PEM encoded X.509 public certificate to the signing certificate chain. - /// - /// This is like [Self::chain_certificate] except the certificate is - /// specified as PEM encoded data. This is a human readable string like - /// `-----BEGIN CERTIFICATE-----` and is a common method for encoding certificate data. - /// (PEM is effectively base64 encoded DER data.) - /// - /// Only a single certificate is read from the PEM data. - pub fn chain_certificate_pem( - &mut self, - data: impl AsRef<[u8]>, - ) -> Result<(), AppleCodesignError> { - self.chain_certificate(CapturedX509Certificate::from_pem(data.as_ref())?); - - Ok(()) - } - - /// Obtain the Time-Stamp Protocol server URL. - pub fn time_stamp_url(&self) -> Option<&Url> { - self.time_stamp_url.as_ref() - } - - /// Set the Time-Stamp Protocol server URL to use to generate a Time-Stamp Token. - /// - /// When set and a signing key-pair is defined, the server will be contacted during - /// signing and a Time-Stamp Token will be embedded in the cryptographic signature. - /// This Time-Stamp Token is a cryptographic proof that someone in possession of - /// the signing key-pair produced the cryptographic signature at a given time. It - /// facilitates validation of the signing time via an independent (presumably trusted) - /// entity. - pub fn set_time_stamp_url(&mut self, url: impl IntoUrl) -> Result<(), AppleCodesignError> { - self.time_stamp_url = Some(url.into_url()?); - - Ok(()) - } - - /// Obtain the team identifier for signed binaries. - pub fn team_id(&self) -> Option<&str> { - self.team_id.get(&SettingsScope::Main).map(|x| x.as_str()) - } - - /// Set the team identifier for signed binaries. - pub fn set_team_id(&mut self, value: impl ToString) { - self.team_id.insert(SettingsScope::Main, value.to_string()); - } - - /// Attempt to set the team ID from the signing certificate. - /// - /// Apple signing certificates have the team ID embedded within the certificate. - /// By calling this method, the team ID embedded within the certificate will - /// be propagated to the code signature. - /// - /// Callers will typically want to call this after registering the signing - /// certificate with [Self::set_signing_key()] but before specifying an explicit - /// team ID via [Self::set_team_id()]. - /// - /// Calling this will replace a registered team IDs if the signing - /// certificate contains a team ID. If no signing certificate is registered or - /// it doesn't contain a team ID, no changes will be made. - /// - /// Returns `Some` if a team ID was set from the signing certificate or `None` - /// otherwise. - pub fn set_team_id_from_signing_certificate(&mut self) -> Option<&str> { - if let Some((_, cert)) = &self.signing_key { - if let Some(team_id) = cert.apple_team_id() { - self.set_team_id(team_id); - Some( - self.team_id - .get(&SettingsScope::Main) - .expect("we just set a team id"), - ) - } else { - None - } - } else { - None - } - } - - /// Return relative paths that should be excluded from signing. - /// - /// Values are glob pattern matches as defined the by `glob` crate. - pub fn path_exclusion_patterns(&self) -> &[Pattern] { - &self.path_exclusion_patterns - } - - /// Add a path to the exclusions list. - pub fn add_path_exclusion(&mut self, v: &str) -> Result<(), AppleCodesignError> { - self.path_exclusion_patterns.push(Pattern::new(v)?); - Ok(()) - } - - /// Obtain the binary identifier string for a given scope. - pub fn binary_identifier(&self, scope: impl AsRef) -> Option<&str> { - self.identifiers.get(scope.as_ref()).map(|s| s.as_str()) - } - - /// Set the binary identifier string for a binary at a path. - /// - /// This only has an effect when signing an individual Mach-O file (use the `None` path) - /// or the non-main executable in a bundle: when signing the main executable in a bundle, - /// the binary's identifier is retrieved from the mandatory `CFBundleIdentifier` value in - /// the bundle's `Info.plist` file. - /// - /// The binary identifier should be a DNS-like name and should uniquely identify the - /// binary. e.g. `com.example.my_program` - pub fn set_binary_identifier(&mut self, scope: SettingsScope, value: impl ToString) { - self.identifiers.insert(scope, value.to_string()); - } - - /// Obtain the entitlements plist as a [plist::Value]. - /// - /// The value should be a [plist::Value::Dictionary] variant. - pub fn entitlements_plist(&self, scope: impl AsRef) -> Option<&plist::Value> { - self.entitlements.get(scope.as_ref()) - } - - /// Obtain the entitlements XML string for a given scope. - pub fn entitlements_xml( - &self, - scope: impl AsRef, - ) -> Result, AppleCodesignError> { - if let Some(value) = self.entitlements_plist(scope) { - let mut buffer = vec![]; - let writer = std::io::Cursor::new(&mut buffer); - value - .to_writer_xml(writer) - .map_err(AppleCodesignError::PlistSerializeXml)?; - - Ok(Some( - String::from_utf8(buffer).expect("plist XML serialization should produce UTF-8"), - )) - } else { - Ok(None) - } - } - - /// Set the entitlements to sign via an XML string. - /// - /// The value should be an XML plist. The value is parsed and stored as - /// a native plist value. - pub fn set_entitlements_xml( - &mut self, - scope: SettingsScope, - value: impl ToString, - ) -> Result<(), AppleCodesignError> { - let cursor = std::io::Cursor::new(value.to_string().into_bytes()); - let value = - plist::Value::from_reader_xml(cursor).map_err(AppleCodesignError::PlistParseXml)?; - - self.entitlements.insert(scope, value); - - Ok(()) - } - - /// Obtain the designated requirements for a given scope. - pub fn designated_requirement( - &self, - scope: impl AsRef, - ) -> &DesignatedRequirementMode { - self.designated_requirement - .get(scope.as_ref()) - .unwrap_or(&DesignatedRequirementMode::Auto) - } - - /// Set the designated requirement for a Mach-O binary given a [CodeRequirementExpression]. - /// - /// The designated requirement (also known as "code requirements") specifies run-time - /// requirements for the binary. e.g. you can stipulate that the binary must be - /// signed by a certificate issued/signed/chained to Apple. The designated requirement - /// is embedded in Mach-O binaries and signed. - 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(()) - } - - /// Set the designated requirement expression for a Mach-O binary given serialized bytes. - /// - /// This is like [SigningSettings::set_designated_requirement_expression] except the - /// designated requirement expression is given as serialized bytes. The bytes passed are - /// the value that would be produced by compiling a code requirement expression via - /// `csreq -b`. - 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::, AppleCodesignError>>()?, - ), - ); - - Ok(()) - } - - /// Set the designated requirement mode to auto, which will attempt to derive requirements - /// automatically. - /// - /// This setting recognizes when code signing is being performed with Apple issued code signing - /// certificates and automatically applies appropriate settings for the certificate being - /// used and the entity being signed. - /// - /// Not all combinations may be supported. If you get an error, you will need to - /// provide your own explicit requirement expression. - pub fn set_auto_designated_requirement(&mut self, scope: SettingsScope) { - self.designated_requirement - .insert(scope, DesignatedRequirementMode::Auto); - } - - /// Obtain the code signature flags for a given scope. - pub fn code_signature_flags( - &self, - scope: impl AsRef, - ) -> Option { - self.code_signature_flags.get(scope.as_ref()).copied() - } - - /// Set code signature flags for signed Mach-O binaries. - /// - /// The incoming flags will replace any already-defined flags. - pub fn set_code_signature_flags(&mut self, scope: SettingsScope, flags: CodeSignatureFlags) { - self.code_signature_flags.insert(scope, flags); - } - - /// Add code signature flags. - /// - /// The incoming flags will be ORd with any existing flags for the path - /// specified. The new flags will be returned. - 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 - } - - /// Remove code signature flags. - /// - /// The incoming flags will be removed from any existing flags for the path - /// specified. The new flags will be returned. - 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 - } - - /// Obtain the `Info.plist` data registered to a given scope. - pub fn info_plist_data(&self, scope: impl AsRef) -> Option<&[u8]> { - self.info_plist_data - .get(scope.as_ref()) - .map(|x| x.as_slice()) - } - - /// Obtain the runtime version for a given scope. - /// - /// The runtime version represents an OS version. - pub fn runtime_version(&self, scope: impl AsRef) -> Option<&semver::Version> { - self.runtime_version.get(scope.as_ref()) - } - - /// Set the runtime version to use in the code directory for a given scope. - /// - /// The runtime version corresponds to an OS version. The runtime version is usually - /// derived from the SDK version used to build the binary. - pub fn set_runtime_version(&mut self, scope: SettingsScope, version: semver::Version) { - self.runtime_version.insert(scope, version); - } - - /// Define the `Info.plist` content. - /// - /// Signatures can reference the digest of an external `Info.plist` file in - /// the bundle the binary is located in. - /// - /// This function registers the raw content of that file is so that the - /// content can be digested and the digest can be included in the code directory. - /// - /// The value passed here should be the raw content of the `Info.plist` XML file. - /// - /// When signing bundles, this function is called automatically with the `Info.plist` - /// from the bundle. This function exists for cases where you are signing - /// individual Mach-O binaries and the `Info.plist` cannot be automatically - /// discovered. - pub fn set_info_plist_data(&mut self, scope: SettingsScope, data: Vec) { - self.info_plist_data.insert(scope, data); - } - - /// Obtain the `CodeResources` XML file data registered to a given scope. - pub fn code_resources_data(&self, scope: impl AsRef) -> Option<&[u8]> { - self.code_resources_data - .get(scope.as_ref()) - .map(|x| x.as_slice()) - } - - /// Define the `CodeResources` XML file content for a given scope. - /// - /// Bundles may contain a `CodeResources` XML file which defines additional - /// resource files and binaries outside the bundle's main executable. The code - /// directory of the main executable contains a digest of this file to establish - /// a chain of trust of the content of this XML file. - /// - /// This function defines the content of this external file so that the content - /// can be digested and that digest included in the code directory of the - /// binary being signed. - /// - /// When signing bundles, this function is called automatically with the content - /// of the `CodeResources` XML file, if present. This function exists for cases - /// where you are signing individual Mach-O binaries and the `CodeResources` XML - /// file cannot be automatically discovered. - pub fn set_code_resources_data(&mut self, scope: SettingsScope, data: Vec) { - self.code_resources_data.insert(scope, data); - } - - /// Obtain extra digests to include in signatures. - pub fn extra_digests(&self, scope: impl AsRef) -> Option<&BTreeSet> { - self.extra_digests.get(scope.as_ref()) - } - - /// Register an addition content digest to use in signatures. - /// - /// Extra digests supplement the primary registered digest when the signer supports - /// it. Calling this likely results in an additional code directory being included - /// in embedded signatures. - /// - /// A common use case for this is to have the primary digest contain a legacy - /// digest type (namely SHA-1) but include stronger digests as well. This enables - /// signatures to have compatibility with older operating systems but still be modern. - pub fn add_extra_digest(&mut self, scope: SettingsScope, digest_type: DigestType) { - self.extra_digests - .entry(scope) - .or_default() - .insert(digest_type); - } - - /// Obtain all configured digests for a scope. - pub fn all_digests(&self, scope: SettingsScope) -> Vec { - let mut res = vec![self.digest_type]; - - if let Some(extra) = self.extra_digests(scope) { - res.extend(extra.iter()); - } - - res - } - - /// Import existing state from Mach-O data. - /// - /// This will synchronize the signing settings with the state in the Mach-O file. - /// - /// If existing settings are explicitly set, they will be honored. Otherwise the state from - /// the Mach-O is imported into the settings. - pub fn import_settings_from_macho(&mut self, data: &[u8]) -> Result<(), AppleCodesignError> { - info!("inferring default signing settings from Mach-O binary"); - - for macho in MachFile::parse(data)?.into_iter() { - let index = macho.index.unwrap_or(0); - - let scope_main = SettingsScope::Main; - let scope_index = SettingsScope::MultiArchIndex(index); - let scope_arch = SettingsScope::MultiArchCpuType(macho.macho.header.cputype()); - - // Older operating system versions don't have support for SHA-256 in - // signatures. If the minimum version targeting in the binary doesn't - // support SHA-256, we automatically change the digest targeting settings - // so the binary will be signed correctly. - if let Some(targeting) = macho.find_targeting()? { - let sha256_version = targeting.platform.sha256_digest_support()?; - - if !sha256_version.matches(&targeting.minimum_os_version) { - info!( - "activating SHA-1 digests because minimum OS target {} is not {}", - targeting.minimum_os_version, sha256_version - ); - - // This logic is a bit wonky. We want SHA-1 to be present on all binaries - // within a fat binary. So if we need SHA-1 mode, we set the setting on the - // main scope and then clear any overrides on fat binary scopes so our - // settings are canonical. - self.set_digest_type(DigestType::Sha1); - self.add_extra_digest(scope_main.clone(), DigestType::Sha256); - self.extra_digests.remove(&scope_arch); - self.extra_digests.remove(&scope_index); - } - } - - // The Mach-O can have embedded Info.plist data. Use it if available and not - // already defined in settings. - if let Some(info_plist) = macho.embedded_info_plist()? { - if self.info_plist_data(&scope_main).is_some() - || self.info_plist_data(&scope_index).is_some() - || self.info_plist_data(&scope_arch).is_some() - { - info!("using Info.plist data from settings"); - } else { - info!("preserving Info.plist data already present in Mach-O"); - self.set_info_plist_data(scope_index.clone(), info_plist); - } - } - - if let Some(sig) = macho.code_signature()? { - if let Some(cd) = sig.code_directory()? { - if self.binary_identifier(&scope_main).is_some() - || self.binary_identifier(&scope_index).is_some() - || self.binary_identifier(&scope_arch).is_some() - { - info!("using binary identifier from settings"); - } else { - info!("preserving existing binary identifier in Mach-O"); - self.set_binary_identifier(scope_index.clone(), cd.ident); - } - - if self.team_id.contains_key(&scope_main) - || self.team_id.contains_key(&scope_index) - || self.team_id.contains_key(&scope_arch) - { - info!("using team ID from settings"); - } else if let Some(team_id) = cd.team_name { - info!("preserving team ID in existing Mach-O signature"); - self.team_id - .insert(scope_index.clone(), team_id.to_string()); - } - - if self.code_signature_flags(&scope_main).is_some() - || self.code_signature_flags(&scope_index).is_some() - || self.code_signature_flags(&scope_arch).is_some() - { - info!("using code signature flags from settings"); - } else if !cd.flags.is_empty() { - info!("preserving code signature flags in existing Mach-O signature"); - self.set_code_signature_flags(scope_index.clone(), cd.flags); - } - - if self.runtime_version(&scope_main).is_some() - || self.runtime_version(&scope_index).is_some() - || self.runtime_version(&scope_arch).is_some() - { - info!("using runtime version from settings"); - } else if let Some(version) = cd.runtime { - info!("preserving runtime version in existing Mach-O signature"); - self.set_runtime_version( - scope_index.clone(), - parse_version_nibbles(version), - ); - } - } - - if let Some(entitlements) = sig.entitlements()? { - if self.entitlements_plist(&scope_main).is_some() - || self.entitlements_plist(&scope_index).is_some() - || self.entitlements_plist(&scope_arch).is_some() - { - info!("using entitlements from settings"); - } else { - info!("preserving existing entitlements in Mach-O"); - self.set_entitlements_xml( - SettingsScope::MultiArchIndex(index), - entitlements.as_str(), - )?; - } - } - } - } - - Ok(()) - } - - /// Convert this instance to settings appropriate for a nested bundle. - #[must_use] - pub fn as_nested_bundle_settings(&self, bundle_path: &str) -> Self { - self.clone_strip_prefix(bundle_path, format!("{}/", bundle_path)) - } - - /// Convert this instance to settings appropriate for a Mach-O binary in a bundle. - #[must_use] - pub fn as_bundle_macho_settings(&self, path: &str) -> Self { - self.clone_strip_prefix(path, path.to_string()) - } - - /// Convert this instance to settings appropriate for a nested Mach-O binary. - /// - /// It is assumed the main scope of these settings is already targeted for - /// a Mach-O binary. Any scoped settings for the Mach-O binary index and CPU type - /// will be applied. CPU type settings take precedence over index scoped settings. - #[must_use] - 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 - } - }) - } - - // Clones this instance, promoting `main_path` to the main scope and stripping - // a prefix from other keys. - 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, - ) -> 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, - path_exclusion_patterns: self.path_exclusion_patterns.clone(), - identifiers: self - .identifiers - .clone() - .into_iter() - .filter_map(|(key, value)| key_map(key).map(|key| (key, value))) - .collect::>(), - entitlements: self - .entitlements - .clone() - .into_iter() - .filter_map(|(key, value)| key_map(key).map(|key| (key, value))) - .collect::>(), - designated_requirement: self - .designated_requirement - .clone() - .into_iter() - .filter_map(|(key, value)| key_map(key).map(|key| (key, value))) - .collect::>(), - code_signature_flags: self - .code_signature_flags - .clone() - .into_iter() - .filter_map(|(key, value)| key_map(key).map(|key| (key, value))) - .collect::>(), - runtime_version: self - .runtime_version - .clone() - .into_iter() - .filter_map(|(key, value)| key_map(key).map(|key| (key, value))) - .collect::>(), - info_plist_data: self - .info_plist_data - .clone() - .into_iter() - .filter_map(|(key, value)| key_map(key).map(|key| (key, value))) - .collect::>(), - code_resources_data: self - .code_resources_data - .clone() - .into_iter() - .filter_map(|(key, value)| key_map(key).map(|key| (key, value))) - .collect::>(), - extra_digests: self - .extra_digests - .clone() - .into_iter() - .filter_map(|(key, value)| key_map(key).map(|key| (key, value))) - .collect::>(), - } - } -} - -#[cfg(test)] -mod tests { - use {super::*, indoc::indoc}; - - const ENTITLEMENTS_XML: &str = indoc! {r#" - - - - - application-identifier - appid - com.apple.developer.team-identifier - ABCDEF - - - "#}; - - #[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_info_plist_data(SettingsScope::MultiArchIndex(0), b"index_0".to_vec()); - main_settings.set_info_plist_data( - SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64), - b"cpu_x86_64".to_vec(), - ); - - 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.info_plist_data(SettingsScope::Main), - Some(b"index_0".as_ref()) - ); - - 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.info_plist_data(SettingsScope::Main), - Some(b"cpu_x86_64".as_ref()) - ); - } - - #[test] - fn as_bundle_macho_settings() { - let mut main_settings = SigningSettings::default(); - main_settings.set_info_plist_data(SettingsScope::Main, b"main".to_vec()); - main_settings.set_info_plist_data( - SettingsScope::Path("Contents/MacOS/main".into()), - b"main_exe".to_vec(), - ); - main_settings.set_info_plist_data( - SettingsScope::PathMultiArchIndex("Contents/MacOS/main".into(), 0), - b"main_exe_index_0".to_vec(), - ); - main_settings.set_info_plist_data( - SettingsScope::PathMultiArchCpuType("Contents/MacOS/main".into(), CPU_TYPE_X86_64), - b"main_exe_x86_64".to_vec(), - ); - - let macho_settings = main_settings.as_bundle_macho_settings("Contents/MacOS/main"); - assert_eq!( - macho_settings.info_plist_data(SettingsScope::Main), - Some(b"main_exe".as_ref()) - ); - assert_eq!( - macho_settings.info_plist_data, - [ - (SettingsScope::Main, b"main_exe".to_vec()), - ( - SettingsScope::MultiArchIndex(0), - b"main_exe_index_0".to_vec() - ), - ( - SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64), - b"main_exe_x86_64".to_vec() - ), - ] - .iter() - .cloned() - .collect::>>() - ); - } - - #[test] - fn as_nested_bundle_settings() { - let mut main_settings = SigningSettings::default(); - main_settings.set_info_plist_data(SettingsScope::Main, b"main".to_vec()); - main_settings.set_info_plist_data( - SettingsScope::Path("Contents/MacOS/main".into()), - b"main_exe".to_vec(), - ); - main_settings.set_info_plist_data( - SettingsScope::Path("Contents/MacOS/nested.app".into()), - b"bundle".to_vec(), - ); - main_settings.set_info_plist_data( - SettingsScope::PathMultiArchIndex("Contents/MacOS/nested.app".into(), 0), - b"bundle_index_0".to_vec(), - ); - main_settings.set_info_plist_data( - SettingsScope::PathMultiArchCpuType( - "Contents/MacOS/nested.app".into(), - CPU_TYPE_X86_64, - ), - b"bundle_x86_64".to_vec(), - ); - main_settings.set_info_plist_data( - SettingsScope::Path("Contents/MacOS/nested.app/Contents/MacOS/nested".into()), - b"nested_main_exe".to_vec(), - ); - main_settings.set_info_plist_data( - SettingsScope::PathMultiArchIndex( - "Contents/MacOS/nested.app/Contents/MacOS/nested".into(), - 0, - ), - b"nested_main_exe_index_0".to_vec(), - ); - main_settings.set_info_plist_data( - SettingsScope::PathMultiArchCpuType( - "Contents/MacOS/nested.app/Contents/MacOS/nested".into(), - CPU_TYPE_X86_64, - ), - b"nested_main_exe_x86_64".to_vec(), - ); - - let bundle_settings = main_settings.as_nested_bundle_settings("Contents/MacOS/nested.app"); - assert_eq!( - bundle_settings.info_plist_data(SettingsScope::Main), - Some(b"bundle".as_ref()) - ); - assert_eq!( - bundle_settings.info_plist_data(SettingsScope::Path("Contents/MacOS/nested".into())), - Some(b"nested_main_exe".as_ref()) - ); - assert_eq!( - bundle_settings.info_plist_data, - [ - (SettingsScope::Main, b"bundle".to_vec()), - (SettingsScope::MultiArchIndex(0), b"bundle_index_0".to_vec()), - ( - SettingsScope::MultiArchCpuType(CPU_TYPE_X86_64), - b"bundle_x86_64".to_vec() - ), - ( - SettingsScope::Path("Contents/MacOS/nested".into()), - b"nested_main_exe".to_vec() - ), - ( - SettingsScope::PathMultiArchIndex("Contents/MacOS/nested".into(), 0), - b"nested_main_exe_index_0".to_vec() - ), - ( - SettingsScope::PathMultiArchCpuType( - "Contents/MacOS/nested".into(), - CPU_TYPE_X86_64 - ), - b"nested_main_exe_x86_64".to_vec() - ), - ] - .iter() - .cloned() - .collect::>>() - ); - } - - #[test] - fn entitlements_handling() -> Result<(), AppleCodesignError> { - let mut settings = SigningSettings::default(); - settings.set_entitlements_xml(SettingsScope::Main, ENTITLEMENTS_XML)?; - - let s = settings.entitlements_xml(SettingsScope::Main)?; - assert_eq!(s, Some("\n\n\n\n\tapplication-identifier\n\tappid\n\tcom.apple.developer.team-identifier\n\tABCDEF\n\n".into())); - - Ok(()) - } -} diff --git a/apple-codesign/src/specification.rs b/apple-codesign/src/specification.rs deleted file mode 100644 index 6174cc4df..000000000 --- a/apple-codesign/src/specification.rs +++ /dev/null @@ -1,324 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -/*! Apple code signing technical specifications - -This document outlines how Apple code signing is implemented at a technical -level. - -# High Level Overview - -Mach-O binaries embed an optional binary blob containing code signing -metadata. This binary blob contains content digests of various aspects -of the binary (such as the executable code) as well as an optional -cryptographic signature which effectively attests to the digested -content of the binary. - -At run-time, stored digests are used to help ensure file integrity. - -The cryptographic signature is used to verify the digests haven't -been tampered with as well as to validate trust with the entity that -produced that signature. - -See - -for an additional overview of how code signing works on Apple platforms. - -# The Important Data Structures - -Mach-O is the executable binary format used by Apple platforms. A -Mach-O binary contains (among other things), a series of named *segments* -holding arbitrary data and *load commands* instructing the loader how -to load/execute the binary. - -Code signing data is embedded within the `__LINKEDIT` segment in a Mach-O -binary. An `LC_CODE_SIGNATURE` load command identifies the offsets of -code signing data within `__LINKEDIT`. - -The code signing data within a `__LINKEDIT` segment is itself a collection -of sub-records. A *SuperBlob* header defines the signing data format, the -length of data to follow, and the number of sub-sections, or *Blob* within. -Each *Blob* occupies a defined *slot*. *Slots* are effectively well-known -pieces of signing data. These include a *Code Directory*, *Entitlements*, -and a *Signature*, among others. See the [crate::CodeSigningSlot] -enumeration for the known defined slots. - -Each *Blob* contains its own header magic effectively identifying the -content type within and how bytes should be interpreted. The magic -values are independent of the *slot* type. However, there appears to be -a relationship between the two. For example, the code directory slot -will have header magic identifying the payload as a code directory structure. - -The *Code Directory* blob/slot defines information about the binary -being signed. There are many fields to this data structure. But the most -important ones to understand are the hashes / content digests. The *Code -Directory* contains digests (e.g. SHA-256) of various content in the binary, -such as Mach-O segment data (i.e. the executable code) and other blobs/slots. - -The *Entitlements* blob/slot contains a *plist*. - -Additional file-based resources can also be signed. These are referred to as -*Code Resources*. *Code Resources* are captured in a -`_CodeSignature/CodeResources` XML plist file in the bundle and the digest -of this file is captured by the *Code Directory*. There is a defined -`RESOURCEDIR` slot to hold its digest. However, there is no explicit -magic constant for resources, implying that this data can only be provided -externally and not embedded within the *SuperBlob*. - -The *Signature* blob/slot contains a Cryptographic Message Syntax (CMS) -RFC 5652 defined `SignedData` BER encoded ASN.1 data structure. CMS is -a specification for cryptographically signing arbitrary content. The -`SignedData` structure contains an additional set of *signed attributes* -(think of it as arbitrary extra content to sign), a cryptographic signature -of the signed data, and likely the X.509 certificate of the signer and its -chain of certificate signers. - -# How Signing Works - -Code signing logically consists of the following steps: - -1. Collecting content that needs to be signed/attested/trusted. -2. Computing content digests. -3. Cryptographically signing a message derived from the content digests. -4. Adding signature data to Mach-O binary. - -## Collecting Content - -Embedded code signatures support signing a myriad of data formats. -These include but aren't limited to: - -* The Mach-O data outside the signature data in the `__LINKEDIT` segment. -* Requested entitlements for the binary. -* A code requirement statement / expression. -* Resource files. - -If your binary is already part of a *bundle*, content collection can -occur automatically using heuristics. e.g. the `Contents/Resources` -directory contains additional files whose content should be signed. - -## Computing Content Digests - -Once content has been assembled, a series of digests are computed. - -For the code digests, the Mach-O segments are iterated. The raw segment -data is chunked into *pages* and each hashed separately. This is to allow -code data to be lazily hashed as a page is loaded into the kernel. -(Otherwise you would have to hash often megabytes on process start, which -would add overhead.) - -Code hashes are a bit nuanced. A hash is emitted at segment boundaries. i.e. -hashes don't span across multiple segments. The `__PAGEZERO` segment is -not hashed. The `__LINKEDIT` segment is hashed, but only up to the start -offset of the embedded signature data, if present. - -Other content (such as the entitlements, code requirement statement, and -resource files) are serialized to *Blob* data. The mechanism for this -varies by type. e.g. the entitlements plist is embedded as UTF-8 -data and the code requirement statement is serialized into an expression -tree. The resulting *Blob* is then digested. - -The content digests are then assembled into a *Code Directory* data -structure. Digests of code data are referred to to *code slots* and -digests of other entitles (namely *Blob* data) occupy *special slots*. -The *Code Directory* also contains important other information, such -as describing the hash/digest mechanism used, the page size for code -hashing, and executable limits for the binary. - -The content of the *Code Directory* serialized to a *Blob* is then itself -digested. This value is known as the *code directory hash*. - -## Cryptographic Signing - -A cryptographic signature is produced using the Cryptographic Message -Syntax (CMS) signing mechanism. - -From a high level, CMS takes as inputs: - -* Optional content to sign. -* Optional set of additional attributes (effectively key-value data) to sign. -* A signing key. -* Information about the signing key (including its CA chain). - -From these, CMS will produce a BER encoded ASN.1 blob containing the -cryptographic signature and sufficient metadata to verify it (such -as the signed attributes and information about the signing certificate). - -In CMS speak, the *encapsulated content* being signed is not defined. -However, the `message-digest` signed attribute is the digest of the -*Code Directory* *Blob* data. (This appears to be not compliant with RFC 5652, -which says *encapsulated content* should be present in the *SignedObject* -structure. Omitting the data is likely done to avoid redundant storage -of this data in the Mach-O binary and/or to simplify parsing, as *Code -Directory* data wouldn't be embedded within an ASN.1 stream.) - -In addition, there is a signed attribute for the signing time. There is -also an XML plist defining an array of base64 encoded *Code Directory* -hashes. There are multiple *slots* in a *SuperBlob* for code directories -and the array in the signed XML plist appears to allow hashes of all of -them to be recorded. - -(TODO it isn't clear what the signed content is when there are multiple -*Code Directory* slots in use. Presumably `message-digest` is computed -over all of them.) - -CMS will concatenate the *Code Directory* data with the DER serialized -ASN.1 structures defining the *signed attributes*. This becomes the -*plaintext* message to be signed. - -This *plaintext* message is combined with a private key and cryptographically -signed (likely using RSA). This produces a *signature*. - -CMS then serializes the *signature*, *signed attributes*, signer -certificate info, and other important metadata to a BER encoded ASN.1 -data structure. This raw slice of bytes is referred to as the -*embedded signature*. - -## Adding Signature Data to Mach-O Binary - -The above steps have already materialized several *Blob* data -structures. The individual pieces like the entitlements and code requirement -*Blob* were materialized in order to compute their hashes for the *Code -Directory* data structure. And the *Code Directory* *Blob* was constructed -so it could be signed by CMS. - -The *embedded signature* data produced by CMS is assembled into a *Blob* -structure. At this point, we have all the *Blob* ready. - -All the *Blobs* are assembled together into a *SuperBlob*. The -*SuperBlob* is then written to the `__LINKEDIT` segment of the -Mach-O binary. An appropriate `LC_CODE_SIGNATURE` load command is -also written to the Mach-O binary to instruct where the *SuperBlob* -data resides. - -The `__LINKEDIT` segment is the last segment in the Mach-O binary and -the *SuperBlob* often occupies the final bytes of the `__LINKEDIT` -segment. So in many cases adding code signature data to a Mach-O -requires an optional truncation to remove the existing signature then -file appends for the `__LINKEDIT` data. - -However, insertion or removal of `LC_CODE_SIGNATURE` will require -rewriting the entire file and adjusting offsets in various Mach-O -data structures accordingly. In many cases, an existing code signature -can be replaced by truncating the `__LINKEDIT` section, writing the -replacement data, and updating sizes/offsets in-place in the segments -index and `LC_CODE_SIGNATURE` load command. - -Note that there is a chicken-and-egg problem related to writing the -Mach-O binary and computing the digests of that binary for the *Code -Directory*! The *Code Directory* needs to compute a digest over the -content of the Mach-O file up until the signature data. But this needs -to be done before a CMS signature is produced, as we need to digest -the *Code Directory* for a CMS signed attribute. We also need to know -the size of the CMS signature, as it is part of the signature data -embedded in the Mach-O binary and its size needs to be recorded in -the `LC_CODE_SIGNATURE` load command and segment definitions, which -are hashed by the *Code Directory*. This is a circular dependency. A -trick to working around it is to pad the Mach-O signature data with -extra NULLs and record this extra long value in `LC_CODE_SIGNATURE` -before code digests are computed. The *SuperBlob* parser appears to -be lenient about this solution. Further note that calculating the -exact final length before CMS signature generation may be impossible -due to the CMS signature being non-deterministic (due to the use of -signing times and timestamp servers tokens, which could be variable -length). - -# How Bundle Signing Works - -Signing bundles (e.g. `.app`, `.framework` directories) has its own -complexities beyond signing individual binaries. - -Bundles consist of multiple files, perhaps multiple binaries. These files -can be classified as: - -1. The main executable. -2. The `Info.plist` file. -3. Support/resources files. -4. Code signature files. - -When signing bundles, the high-level process is the following: - -1. Find and sign all nested binaries and bundles (bundles can contain - other bundles) except the main binary and bundle. -2. Identify support/resources files and calculate their hashes, capturing - this metadata in a `CodeResources` XML file. -3. Sign the main binary with an embedded reference to the digest of the - `CodeResources` file. - -# How Verification Works - -What happens when a binary is loaded? Read on to find out. - -Please note that we don't know for sure what all occurs when a binary is -loaded because the code is proprietary. We do have some high-level -documentation from Apple and we can empirically observe what occurs. -We can also infer what is happening based on the signing technical -implementation, assuming Apple follows correct practices. But some content -of this section is speculation and is merely what *likely* occurs. - -When a Mach-O binary is loaded, the loader looks for an -`LC_CODE_SIGNATURE` load command. If not found, there is no embedded -signature data and running the binary may be rejected. - -The associated code signature data is located in the `__LINKEDIT` section -and parsed so *Blob* are discovered. How deeply it is parsed at this stage, -we don't know. - -Data for the *Signature* slot/blob is obtained. This is the CMS *SignedData* -structure (BER encoded ASN.1). This structure is decoded and the cryptographic -signature, signed attributes, and X.509 certificates involved in the signing -are obtained from within. - -We do not know the full extent of trust verification that occurs. But -Apple will examine details of the signing certificate and ensure its use -is allowed. For example, if the signing certificate wasn't issued/signed -by Apple or doesn't have the appropriate extensions present (such as bits -indicating the certificate is appropriate for code signing), it may refuse -to proceed. This trust validation likely occurs immediately after the -CMS data is parsed, as soon as the signing certificate information becomes -available for scrutiny. - -The original *plaintext* message that was signed is assembled. This is -done by DER encoding the *signed attributes* from the CMS *SignedData* -structure. - -This *plaintext* message, the signature of it, and the public key used -to produce the signature are all used to verify the cryptographic integrity -of the *signed attributes*. This effectively answers the question *did -something with possession of certificate X sign exactly the signed attributes -in this message.* - -Successful signature verification ensures that the *signed attributes* -haven't been tampered with since they were signed. - -The CMS data may also contain *unsigned attributes*. There may be -a *time stamp token* here containing a signature of the time when the -signed message was produced. This may be validated as well. - -One of the signed attributes is `message-digest`. In this use of CMS, -`message-digest` is the digest of the *Code Directory* *Blob* data. This -digest is possibly verified: we don't know for sure. According to RFC 5652 -it should be verified. However, it may not need to be because the digest -of the *Code Directory* data is stored elsewhere... - -A signed attribute contains an XML plist containing an array of base64 encoded -hashes of *Code Directory* *blobs*. This plist is likely parsed and the hashes -within are compared to the hashes from the *Code Directory* blobs/slots from -the *SuperBlob* record. If the digests are identical, it means that the *Code -Directory* data structures in the Mach-O binary haven't been modified since the -signature was created. - -The *Code Directory* data structures contain digests of code data and -other *Blob* data from the *SuperBlob*. Since the digest of the *Code Directory* -data was verified via CMS and a trust relationship was (presumably) established -with the signer of that CMS data, verification and trust is transitively applied -to the other *Blob* data and code data (this is effectively a Merkle Tree). -This means that we can digest other *Blob* entries and code data and compare to -the digests within the *Code Directory* structures. If the digests are identical, -content hasn't changed since the signature was made. - -It is unclear in what order other *Blob* data is read. But presumably important -data like the embedded entitlements and code requirement statement are read very -early during binary loading so an appropriate trust policy can be applied to -the binary. -*/ diff --git a/apple-codesign/src/stapling.rs b/apple-codesign/src/stapling.rs deleted file mode 100644 index b540cb70d..000000000 --- a/apple-codesign/src/stapling.rs +++ /dev/null @@ -1,334 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -/*! Attach Apple notarization tickets to signed entities. - -Stapling refers to the act of taking an Apple issued notarization -ticket (generated after uploading content to Apple for inspection) -and attaching that ticket to the entity that was uploaded. The -mechanism varies, but stapling is literally just fetching a payload -from Apple and attaching it to something else. -*/ - -use { - crate::{ - bundle_signing::SignedMachOInfo, - dmg::{DmgReader, DmgSigner}, - embedded_signature::{Blob, DigestType}, - reader::PathType, - ticket_lookup::{default_client, lookup_notarization_ticket}, - AppleCodesignError, - }, - apple_bundles::{BundlePackageType, DirectoryBundle}, - apple_xar::reader::XarReader, - log::{error, info, warn}, - reqwest::blocking::Client, - scroll::{IOread, IOwrite, Pread, Pwrite, SizeWith}, - std::{ - fmt::Debug, - fs::File, - io::{Read, Seek, SeekFrom, Write}, - path::Path, - }, -}; - -/// Resolve the notarization ticket record name from a bundle. -/// -/// The record name is derived from the digest of the code directory of the -/// main binary within the bundle. -pub fn record_name_from_app_bundle(bundle: &DirectoryBundle) -> Result { - if !matches!(bundle.package_type(), BundlePackageType::App) { - return Err(AppleCodesignError::StapleUnsupportedBundleType( - bundle.package_type(), - )); - } - - let main_exe = bundle - .files(false) - .map_err(AppleCodesignError::DirectoryBundle)? - .into_iter() - .find(|file| matches!(file.is_main_executable(), Ok(true))) - .ok_or(AppleCodesignError::StapleMainExecutableNotFound)?; - - // Now extract the code signature so we can resolve the code directory. - info!( - "resolving bundle's record name from {}", - main_exe.absolute_path().display() - ); - let macho_data = std::fs::read(main_exe.absolute_path())?; - - let signed = SignedMachOInfo::parse_data(&macho_data)?; - - let record_name = signed.notarization_ticket_record_name()?; - - Ok(record_name) -} - -/// Staple a ticket to a bundle as defined by the path to a directory. -/// -/// Stapling a bundle (e.g. `MyApp.app`) is literally just writing a -/// `Contents/CodeResources` file containing the raw ticket data. -pub fn staple_ticket_to_bundle( - bundle: &DirectoryBundle, - ticket_data: &[u8], -) -> Result<(), AppleCodesignError> { - let path = bundle.resolve_path("CodeResources"); - - warn!("writing notarization ticket to {}", path.display()); - std::fs::write(&path, ticket_data)?; - - Ok(()) -} - -/// Magic header for xar trailer struct. -/// -/// `t8lr`. -const XAR_NOTARIZATION_TRAILER_MAGIC: [u8; 4] = [0x74, 0x38, 0x6c, 0x72]; - -#[derive(Clone, Copy, Debug, IOread, IOwrite, Pread, Pwrite, SizeWith)] -pub struct XarNotarizationTrailer { - /// "t8lr" - pub magic: [u8; 4], - pub version: u16, - pub typ: u16, - pub length: u32, - pub unused: u32, -} - -#[derive(Clone, Copy, Debug)] -#[repr(u16)] -pub enum XarNotarizationTrailerType { - Invalid = 0, - Terminator = 1, - Ticket = 2, -} - -/// Obtain the notarization trailer data for a XAR archive. -/// -/// The trailer data consists of a [XarNotarizationTrailer] of type `Terminator` -/// to denote the end of XAR content followed by the raw ticket data followed by a -/// [XarNotarizationTrailer] with type `Ticket`. Essentially, a reader can look for -/// a ticket trailer at the end of the file then quickly seek to the beginning of -/// ticket data. -pub fn xar_notarization_trailer(ticket_data: &[u8]) -> Result, AppleCodesignError> { - let terminator = XarNotarizationTrailer { - magic: XAR_NOTARIZATION_TRAILER_MAGIC, - version: 1, - typ: XarNotarizationTrailerType::Terminator as u16, - length: 0, - unused: 0, - }; - let ticket = XarNotarizationTrailer { - magic: XAR_NOTARIZATION_TRAILER_MAGIC, - version: 1, - typ: XarNotarizationTrailerType::Ticket as u16, - length: ticket_data.len() as _, - unused: 0, - }; - - let mut cursor = std::io::Cursor::new(Vec::new()); - cursor.iowrite_with(terminator, scroll::LE)?; - cursor.write_all(ticket_data)?; - cursor.iowrite_with(ticket, scroll::LE)?; - - Ok(cursor.into_inner()) -} - -/// Handles stapling operations. -pub struct Stapler { - client: Client, -} - -impl Stapler { - /// Construct a new instance with defaults. - pub fn new() -> Result { - Ok(Self { - client: default_client()?, - }) - } - - /// Set the HTTP client to use for ticket lookups. - pub fn set_client(&mut self, client: Client) { - self.client = client; - } - - /// Look up a notarization ticket for an app bundle. - /// - /// This will resolve the notarization ticket record name from the contents - /// of the bundle then attempt to look up that notarization ticket against - /// Apple's servers. - /// - /// This errors if there is a problem deriving the notarization ticket record name - /// or if a failure occurs when looking up the notarization ticket. This can include - /// a notarization ticket not existing for the requested record. - pub fn lookup_ticket_for_app_bundle( - &self, - bundle: &DirectoryBundle, - ) -> Result, AppleCodesignError> { - let record_name = record_name_from_app_bundle(bundle)?; - - let response = lookup_notarization_ticket(&self.client, &record_name)?; - - let ticket_data = response.signed_ticket(&record_name)?; - - Ok(ticket_data) - } - - /// Attempt to staple a bundle by obtaining a notarization ticket automatically. - pub fn staple_bundle(&self, bundle: &DirectoryBundle) -> Result<(), AppleCodesignError> { - warn!( - "attempting to find notarization ticket for bundle at {}", - bundle.root_dir().display() - ); - let ticket_data = self.lookup_ticket_for_app_bundle(bundle)?; - staple_ticket_to_bundle(bundle, &ticket_data)?; - - Ok(()) - } - - /// Look up ticket data for DMG file. - pub fn lookup_ticket_for_dmg(&self, dmg: &DmgReader) -> Result, AppleCodesignError> { - // The ticket is derived from the code directory digest from the signature in the - // DMG. - let signature = dmg - .embedded_signature()? - .ok_or(AppleCodesignError::DmgStapleNoSignature)?; - let cd = signature - .code_directory()? - .ok_or(AppleCodesignError::DmgStapleNoSignature)?; - - let mut digest = cd.digest_with(cd.digest_type)?; - digest.truncate(20); - let digest = hex::encode(digest); - - let digest_type: u8 = cd.digest_type.into(); - - let record_name = format!("2/{}/{}", digest_type, digest); - - let response = lookup_notarization_ticket(&self.client, &record_name)?; - - response.signed_ticket(&record_name) - } - - /// Attempt to staple a DMG by obtaining a notarization ticket automatically. - pub fn staple_dmg(&self, path: &Path) -> Result<(), AppleCodesignError> { - let mut fh = File::options().read(true).write(true).open(path)?; - - warn!( - "attempting to find notarization ticket for DMG at {}", - path.display() - ); - let reader = DmgReader::new(&mut fh)?; - - let ticket_data = self.lookup_ticket_for_dmg(&reader)?; - warn!("found notarization ticket; proceeding with stapling"); - - let signer = DmgSigner::default(); - signer.staple_file(&mut fh, ticket_data)?; - - Ok(()) - } - - /// Lookup ticket data for a XAR archive (e.g. a `.pkg` file). - pub fn lookup_ticket_for_xar( - &self, - reader: &mut XarReader, - ) -> Result, AppleCodesignError> { - let mut digest = reader.checksum_data()?; - digest.truncate(20); - let digest = hex::encode(digest); - - let digest_type = DigestType::try_from(reader.table_of_contents().checksum.style)?; - let digest_type: u8 = digest_type.into(); - - let record_name = format!("2/{}/{}", digest_type, digest); - - let response = lookup_notarization_ticket(&self.client, &record_name)?; - - response.signed_ticket(&record_name) - } - - /// Staple a XAR archive. - /// - /// Takes the handle to a readable, writable, and seekable object. - /// - /// The stream will be opened as a XAR file. If a ticket is found, that ticket - /// will be appended to the end of the file. - pub fn staple_xar( - &self, - mut xar: XarReader, - ) -> Result<(), AppleCodesignError> { - let ticket_data = self.lookup_ticket_for_xar(&mut xar)?; - - warn!("found notarization ticket; proceeding with stapling"); - - let mut fh = xar.into_inner(); - - // As a convenience, we look for an existing ticket trailer so we can tell - // the user we're effectively overwriting it. We could potentially try to - // delete or overwrite the old trailer. BUt it is just easier to append, - // as a writer likely only looks for the ticket trailer at the tail end - // of the file. - let trailer_size = 16; - fh.seek(SeekFrom::End(-trailer_size))?; - - let trailer = fh.ioread_with::(scroll::LE)?; - if trailer.magic == XAR_NOTARIZATION_TRAILER_MAGIC { - let trailer_type = match trailer.typ { - x if x == XarNotarizationTrailerType::Invalid as u16 => "invalid", - x if x == XarNotarizationTrailerType::Ticket as u16 => "ticket", - x if x == XarNotarizationTrailerType::Terminator as u16 => "terminator", - _ => "unknown", - }; - - warn!("found an existing XAR trailer of type {}", trailer_type); - warn!("this existing trailer will be preserved and will likely be ignored"); - } - - let trailer = xar_notarization_trailer(&ticket_data)?; - - warn!( - "stapling notarization ticket trailer ({} bytes) to end of XAR", - trailer.len() - ); - fh.write_all(&trailer)?; - - Ok(()) - } - - /// Attempt to staple an entity at a given filesystem path. - /// - /// The path will be modified on successful stapling operation. - pub fn staple_path(&self, path: impl AsRef) -> Result<(), AppleCodesignError> { - let path = path.as_ref(); - warn!("attempting to staple {}", path.display()); - - match PathType::from_path(path)? { - PathType::MachO => { - error!("cannot staple Mach-O binaries"); - Err(AppleCodesignError::StapleUnsupportedPath( - path.to_path_buf(), - )) - } - PathType::Dmg => { - warn!("activating DMG stapling mode"); - self.staple_dmg(path) - } - PathType::Bundle => { - warn!("activating bundle stapling mode"); - let bundle = DirectoryBundle::new_from_path(path) - .map_err(AppleCodesignError::DirectoryBundle)?; - self.staple_bundle(&bundle) - } - PathType::Xar => { - warn!("activating XAR stapling mode"); - let xar = XarReader::new(File::options().read(true).write(true).open(path)?)?; - self.staple_xar(xar) - } - PathType::Other => Err(AppleCodesignError::StapleUnsupportedPath( - path.to_path_buf(), - )), - } - } -} diff --git a/apple-codesign/src/testdata/apple-signed-3rd-party-mac.cer b/apple-codesign/src/testdata/apple-signed-3rd-party-mac.cer deleted file mode 100644 index fe5bf80cf..000000000 Binary files a/apple-codesign/src/testdata/apple-signed-3rd-party-mac.cer and /dev/null differ diff --git a/apple-codesign/src/testdata/apple-signed-apple-development.cer b/apple-codesign/src/testdata/apple-signed-apple-development.cer deleted file mode 100644 index 7c9bdf929..000000000 Binary files a/apple-codesign/src/testdata/apple-signed-apple-development.cer and /dev/null differ diff --git a/apple-codesign/src/testdata/apple-signed-apple-distribution.cer b/apple-codesign/src/testdata/apple-signed-apple-distribution.cer deleted file mode 100644 index f3a9e317a..000000000 Binary files a/apple-codesign/src/testdata/apple-signed-apple-distribution.cer and /dev/null differ diff --git a/apple-codesign/src/testdata/apple-signed-developer-id-application.cer b/apple-codesign/src/testdata/apple-signed-developer-id-application.cer deleted file mode 100644 index dca1bfc0d..000000000 Binary files a/apple-codesign/src/testdata/apple-signed-developer-id-application.cer and /dev/null differ diff --git a/apple-codesign/src/testdata/apple-signed-developer-id-installer.cer b/apple-codesign/src/testdata/apple-signed-developer-id-installer.cer deleted file mode 100644 index 6a2de5fcf..000000000 Binary files a/apple-codesign/src/testdata/apple-signed-developer-id-installer.cer and /dev/null differ diff --git a/apple-codesign/src/testdata/ed25519.pk8 b/apple-codesign/src/testdata/ed25519.pk8 deleted file mode 100644 index b7ffa3803..000000000 Binary files a/apple-codesign/src/testdata/ed25519.pk8 and /dev/null differ diff --git a/apple-codesign/src/testdata/rsa-2048.pk8 b/apple-codesign/src/testdata/rsa-2048.pk8 deleted file mode 100644 index 1a1ec9c2d..000000000 Binary files a/apple-codesign/src/testdata/rsa-2048.pk8 and /dev/null differ diff --git a/apple-codesign/src/testdata/secp256r1.pk8 b/apple-codesign/src/testdata/secp256r1.pk8 deleted file mode 100644 index a6961708b..000000000 Binary files a/apple-codesign/src/testdata/secp256r1.pk8 and /dev/null differ diff --git a/apple-codesign/src/ticket_lookup.rs b/apple-codesign/src/ticket_lookup.rs deleted file mode 100644 index 95a613bd0..000000000 --- a/apple-codesign/src/ticket_lookup.rs +++ /dev/null @@ -1,248 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -/*! Support for retrieving notarization tickets and stapling artifacts. */ - -use { - crate::AppleCodesignError, - log::warn, - reqwest::blocking::{Client, ClientBuilder}, - serde::{Deserialize, Serialize}, - std::collections::HashMap, -}; - -/// URL of HTTP service where Apple publishes stapling tickets. -pub const APPLE_TICKET_LOOKUP_URL: &str = "https://api.apple-cloudkit.com/database/1/com.apple.gk.ticket-delivery/production/public/records/lookup"; - -/// Main JSON request object for ticket lookup requests. -#[derive(Clone, Debug, Serialize)] -pub struct TicketLookupRequest { - pub records: Vec, -} - -/// Represents a single record to look up in a ticket lookup request. -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct TicketLookupRequestRecord { - pub record_name: String, -} - -/// Main JSON response object to ticket lookup requests. -#[derive(Clone, Debug, Deserialize)] -pub struct TicketLookupResponse { - pub records: Vec, -} - -impl TicketLookupResponse { - /// Obtain the signed ticket for a given record name. - /// - /// `record_name` is of the form `2//`. e.g. - /// `2/2/deadbeefdeadbeef....`. - /// - /// Returns an `Err` if a signed ticket could not be found. - pub fn signed_ticket(&self, record_name: &str) -> Result, AppleCodesignError> { - let record = self - .records - .iter() - .find(|r| r.record_name() == record_name) - .ok_or_else(|| { - AppleCodesignError::NotarizationRecordNotInResponse(record_name.to_string()) - })?; - - match record { - TicketLookupResponseRecord::Success(r) => r - .signed_ticket_data() - .ok_or(AppleCodesignError::NotarizationRecordNoSignedTicket)?, - TicketLookupResponseRecord::Failure(r) => { - Err(AppleCodesignError::NotarizationLookupFailure( - r.server_error_code.clone(), - r.reason.clone(), - )) - } - } - } -} - -/// Describes the results of a ticket lookup for a specific record. -#[derive(Clone, Debug, Deserialize)] -#[serde(untagged)] -pub enum TicketLookupResponseRecord { - /// Ticket was found. - Success(TicketLookupResponseRecordSuccess), - - /// Some error occurred. - Failure(TicketLookupResponseRecordFailure), -} - -impl TicketLookupResponseRecord { - /// Obtain the record name associated with this record. - pub fn record_name(&self) -> &str { - match self { - Self::Success(r) => &r.record_name, - Self::Failure(r) => &r.record_name, - } - } -} - -/// Represents a successful ticket lookup response record. -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TicketLookupResponseRecordSuccess { - /// Name of record that was looked up. - pub record_name: String, - - pub created: TicketRecordEvent, - pub deleted: bool, - /// Holds data. - /// - /// The `signedTicket` key holds the ticket. - pub fields: HashMap, - pub modified: TicketRecordEvent, - // TODO pluginFields - pub record_change_tag: String, - - /// A value like `DeveloperIDTicket`. - /// - /// We could potentially turn this into an enumeration... - pub record_type: String, -} - -impl TicketLookupResponseRecordSuccess { - /// Obtain the raw signed ticket data in this record. - /// - /// Evaluates to `Some` if there appears to be a signed ticket and `None` - /// otherwise. - /// - /// There can be an inner `Err` if we don't know how to decode the response data - /// or there was an error decoding. - pub fn signed_ticket_data(&self) -> Option, AppleCodesignError>> { - match self.fields.get("signedTicket") { - Some(field) => { - if field.typ == "BYTES" { - Some( - base64::decode(&field.value) - .map_err(AppleCodesignError::NotarizationRecordDecodeFailure), - ) - } else { - Some(Err( - AppleCodesignError::NotarizationRecordSignedTicketNotBytes( - field.typ.clone(), - ), - )) - } - } - None => None, - } - } -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TicketLookupResponseRecordFailure { - pub record_name: String, - pub reason: String, - pub server_error_code: String, -} - -/// Represents an event in a ticket record. -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TicketRecordEvent { - #[serde(rename = "deviceID")] - pub device_id: String, - pub timestamp: u64, - pub user_record_name: String, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct Field { - #[serde(rename = "type")] - pub typ: String, - pub value: String, -} - -/// Obtain the default [Client] to use for HTTP requests. -pub fn default_client() -> Result { - Ok(ClientBuilder::default() - .user_agent("apple-codesign crate (https://crates.io/crates/apple-codesign)") - .build()?) -} - -/// Look up a notarization ticket given an HTTP client and an iterable of record names. -/// -/// The record name is of the form `2//`. -pub fn lookup_notarization_tickets<'a>( - client: &Client, - record_names: impl Iterator, -) -> Result { - let body = TicketLookupRequest { - records: record_names - .map(|x| { - warn!("looking up notarization ticket for {}", x); - TicketLookupRequestRecord { - record_name: x.to_string(), - } - }) - .collect::>(), - }; - - let req = client - .post(APPLE_TICKET_LOOKUP_URL) - .header("Accept", "application/json") - .header("Content-Type", "application/json") - .json(&body); - - let response = req.send()?; - - let body = response.bytes()?; - - let response = serde_json::from_slice::(&body)?; - - Ok(response) -} - -/// Look up a single notarization ticket. -/// -/// This is just a convenience wrapper around [lookup_notarization_tickets()]. -pub fn lookup_notarization_ticket( - client: &Client, - record_name: &str, -) -> Result { - lookup_notarization_tickets(client, std::iter::once(record_name)) -} - -#[cfg(test)] -mod test { - use super::*; - - const PYOXIDIZER_APP_RECORD: &str = "2/2/1b747faf223750de74febed7929f14a73af8c933"; - const DEADBEEF: &str = "2/2/deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; - - #[test] - fn lookup_ticket() -> Result<(), AppleCodesignError> { - let client = default_client()?; - - let res = lookup_notarization_ticket(&client, PYOXIDIZER_APP_RECORD)?; - - assert!(matches!( - &res.records[0], - TicketLookupResponseRecord::Success(_) - )); - - let ticket = res.signed_ticket(PYOXIDIZER_APP_RECORD)?; - assert_eq!(&ticket[0..4], b"s8ch"); - - let res = lookup_notarization_ticket(&client, DEADBEEF)?; - assert!(matches!( - &res.records[0], - TicketLookupResponseRecord::Failure(_) - )); - assert!(matches!( - res.signed_ticket(DEADBEEF), - Err(AppleCodesignError::NotarizationLookupFailure(_, _)) - )); - - Ok(()) - } -} diff --git a/apple-codesign/src/verify.rs b/apple-codesign/src/verify.rs deleted file mode 100644 index c0f31f684..000000000 --- a/apple-codesign/src/verify.rs +++ /dev/null @@ -1,549 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Code signing verification. -//! -//! This module implements functionality for verifying code signatures on -//! Mach-O binaries. -//! -//! # Verification Caveats -//! -//! **Verification performed by this code will vary from what Apple tools -//! do. Do not use successful verification from this code as validation that -//! Apple software will accept a signature.** -//! -//! We aim for our verification code to be as comprehensive as possible. But -//! there are things it doesn't yet or won't ever do. For example, we have -//! no clue of the full extent of verification that Apple performs because -//! that code is proprietary. We know some of the things that are done and -//! we have verification for a subset of them. Read the code or the set of -//! verification problem types enumerated by [VerificationProblemType] to get -//! a sense of what we do. - -use { - crate::{ - code_directory::CodeDirectoryBlob, - embedded_signature::{CodeSigningSlot, DigestType, EmbeddedSignature}, - error::AppleCodesignError, - macho::{MachFile, MachOBinary}, - }, - cryptographic_message_syntax::{CmsError, SignedData}, - std::path::{Path, PathBuf}, - x509_certificate::{DigestAlgorithm, SignatureAlgorithm}, -}; - -/// Context for a verification issue. -#[derive(Clone, Debug)] -pub struct VerificationContext { - /// Path of binary. - pub path: Option, - - /// Index of Mach-O binary within a fat binary that is problematic. - pub fat_index: Option, -} - -/// Describes a problem with verification. -#[derive(Debug)] -pub enum VerificationProblemType { - IoError(std::io::Error), - MachOParseError(AppleCodesignError), - NoMachOSignatureData, - MachOSignatureError(AppleCodesignError), - LinkeditNotLastSegment, - SignatureNotLastLinkeditData, - NoCryptographicSignature, - CmsError(CmsError), - CmsOldDigestAlgorithm(DigestAlgorithm), - CmsOldSignatureAlgorithm(SignatureAlgorithm), - NoCodeDirectory, - CodeDirectoryOldDigestAlgorithm(DigestType), - CodeDigestError(AppleCodesignError), - CodeDigestMissingEntry(usize, Vec), - CodeDigestExtraEntry(usize, Vec), - CodeDigestMismatch(usize, Vec, Vec), - SlotDigestMissing(CodeSigningSlot), - ExtraSlotDigest(CodeSigningSlot, Vec), - SlotDigestMismatch(CodeSigningSlot, Vec, Vec), - SlotDigestError(AppleCodesignError), -} - -#[derive(Debug)] -pub struct VerificationProblem { - pub context: VerificationContext, - pub problem: VerificationProblemType, -} - -impl std::fmt::Display for VerificationProblem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let context = match (&self.context.path, &self.context.fat_index) { - (None, None) => None, - (Some(path), None) => Some(format!("{}", path.display())), - (None, Some(index)) => Some(format!("@{}", index)), - (Some(path), Some(index)) => Some(format!("{}@{}", path.display(), index)), - }; - - let message = match &self.problem { - VerificationProblemType::IoError(e) => format!("I/O error: {}", e), - VerificationProblemType::MachOParseError(e) => format!("Mach-O parse failure: {}", e), - VerificationProblemType::NoMachOSignatureData => { - "Mach-O signature data not found".to_string() - } - VerificationProblemType::MachOSignatureError(e) => { - format!("error parsing Mach-O signature data: {:?}", e) - } - VerificationProblemType::LinkeditNotLastSegment => { - "__LINKEDIT isn't last Mach-O segment".to_string() - } - VerificationProblemType::SignatureNotLastLinkeditData => { - "signature isn't last data in __LINKEDIT segment".to_string() - } - VerificationProblemType::NoCryptographicSignature => { - "no cryptographic signature present".to_string() - } - VerificationProblemType::CmsError(e) => format!("CMS error: {}", e), - VerificationProblemType::CmsOldDigestAlgorithm(alg) => { - format!("insecure digest algorithm used: {:?}", alg) - } - VerificationProblemType::CmsOldSignatureAlgorithm(alg) => { - format!("insecure signature algorithm used: {:?}", alg) - } - VerificationProblemType::NoCodeDirectory => "no code directory".to_string(), - VerificationProblemType::CodeDirectoryOldDigestAlgorithm(hash_type) => { - format!( - "insecure digest algorithm used in code directory: {:?}", - hash_type - ) - } - VerificationProblemType::CodeDigestError(e) => { - format!("error computing code digests: {:?}", e) - } - VerificationProblemType::CodeDigestMissingEntry(index, digest) => { - format!( - "code digest missing entry at index {} for digest {}", - index, - hex::encode(&digest) - ) - } - VerificationProblemType::CodeDigestExtraEntry(index, digest) => { - format!( - "code digest contains extra entry index {} with digest {}", - index, - hex::encode(&digest) - ) - } - VerificationProblemType::CodeDigestMismatch(index, cd_digest, actual_digest) => { - format!( - "code digest mismatch for entry {}; recorded digest {}, actual {}", - index, - hex::encode(&cd_digest), - hex::encode(&actual_digest) - ) - } - VerificationProblemType::SlotDigestMissing(slot) => { - format!("missing digest for slot {:?}", slot) - } - VerificationProblemType::ExtraSlotDigest(slot, digest) => { - format!( - "slot digest contains digest for slot not in signature: {:?} with digest {}", - slot, - hex::encode(digest) - ) - } - VerificationProblemType::SlotDigestMismatch(slot, cd_digest, actual_digest) => { - format!( - "slot digest mismatch for slot {:?}; recorded digest {}, actual {}", - slot, - hex::encode(cd_digest), - hex::encode(actual_digest) - ) - } - VerificationProblemType::SlotDigestError(e) => { - format!("error computing slot digest: {:?}", e) - } - }; - - match context { - Some(context) => f.write_fmt(format_args!("{}: {}", context, message)), - None => f.write_str(&message), - } - } -} - -/// Verifies a binary in a given path. -/// -/// Returns a vector of problems detected. An empty vector means no -/// problems were found. -pub fn verify_path(path: impl AsRef) -> Vec { - let path = path.as_ref(); - - let context = VerificationContext { - path: Some(path.to_path_buf()), - fat_index: None, - }; - - let data = match std::fs::read(path) { - Ok(data) => data, - Err(e) => { - return vec![VerificationProblem { - context, - problem: VerificationProblemType::IoError(e), - }]; - } - }; - - verify_macho_data_internal(data, context) -} - -/// Verifies unparsed Mach-O data. -/// -/// Returns a vector of problems detected. An empty vector means no -/// problems were found. -pub fn verify_macho_data(data: impl AsRef<[u8]>) -> Vec { - let context = VerificationContext { - path: None, - fat_index: None, - }; - - verify_macho_data_internal(data, context) -} - -fn verify_macho_data_internal( - data: impl AsRef<[u8]>, - context: VerificationContext, -) -> Vec { - match MachFile::parse(data.as_ref()) { - Ok(mach) => { - let mut problems = vec![]; - - for macho in mach.into_iter() { - let mut context = context.clone(); - context.fat_index = macho.index; - - problems.extend(verify_macho_internal(&macho, context)); - } - - problems - } - Err(e) => { - vec![VerificationProblem { - context, - problem: VerificationProblemType::MachOParseError(e), - }] - } - } -} - -/// Verifies a parsed Mach-O binary. -/// -/// Returns a vector of problems detected. An empty vector means no -/// problems were found. -pub fn verify_macho(macho: &MachOBinary) -> Vec { - verify_macho_internal( - macho, - VerificationContext { - path: None, - fat_index: None, - }, - ) -} - -fn verify_macho_internal( - macho: &MachOBinary, - context: VerificationContext, -) -> Vec { - let signature_data = match macho.find_signature_data() { - Ok(Some(data)) => data, - Ok(None) => { - return vec![VerificationProblem { - context, - problem: VerificationProblemType::NoMachOSignatureData, - }]; - } - Err(e) => { - return vec![VerificationProblem { - context, - problem: VerificationProblemType::MachOSignatureError(e), - }]; - } - }; - - let mut problems = vec![]; - - // __LINKEDIT segment should be the last segment. - if signature_data.linkedit_segment_index != macho.macho.segments.len() - 1 { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::LinkeditNotLastSegment, - }); - } - - // Signature data should be the last data in the __LINKEDIT segment. - if signature_data.signature_end_offset != signature_data.linkedit_segment_data.len() { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::SignatureNotLastLinkeditData, - }); - } - - let signature = match macho.code_signature() { - Ok(Some(signature)) => signature, - Ok(None) => { - panic!("no signature should have been handled above"); - } - Err(e) => { - problems.push(VerificationProblem { - context, - problem: VerificationProblemType::MachOSignatureError(e), - }); - - // Can't do anything more if we couldn't parse the signature data. - return problems; - } - }; - - match signature.signature_data() { - Ok(Some(cms_blob)) => { - problems.extend(verify_cms_signature(cms_blob, context.clone())); - } - Ok(None) => problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::NoCryptographicSignature, - }), - Err(e) => { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::MachOSignatureError(e), - }); - } - } - - match signature.code_directory() { - Ok(Some(cd)) => { - problems.extend(verify_code_directory(macho, &signature, &cd, context)); - } - Ok(None) => { - problems.push(VerificationProblem { - context, - problem: VerificationProblemType::NoCodeDirectory, - }); - } - Err(e) => { - problems.push(VerificationProblem { - context, - problem: VerificationProblemType::MachOSignatureError(e), - }); - } - } - - problems -} - -fn verify_cms_signature(data: &[u8], context: VerificationContext) -> Vec { - let signed_data = match SignedData::parse_ber(data) { - Ok(signed_data) => signed_data, - Err(e) => { - return vec![VerificationProblem { - context, - problem: VerificationProblemType::CmsError(e), - }]; - } - }; - - let mut problems = vec![]; - - for signer in signed_data.signers() { - match signer.digest_algorithm() { - DigestAlgorithm::Sha1 => { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::CmsOldDigestAlgorithm( - signer.digest_algorithm(), - ), - }); - } - DigestAlgorithm::Sha384 => {} - DigestAlgorithm::Sha256 => {} - DigestAlgorithm::Sha512 => {} - } - - match signer.signature_algorithm() { - SignatureAlgorithm::RsaSha256 - | SignatureAlgorithm::RsaSha384 - | SignatureAlgorithm::RsaSha512 - | SignatureAlgorithm::EcdsaSha256 - | SignatureAlgorithm::EcdsaSha384 - | SignatureAlgorithm::Ed25519 => {} - SignatureAlgorithm::RsaSha1 => { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::CmsOldSignatureAlgorithm( - signer.signature_algorithm(), - ), - }); - } - } - - match signer.verify_signature_with_signed_data(&signed_data) { - Ok(()) => {} - Err(e) => { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::CmsError(e), - }); - } - } - - // TODO verify key length meets standards. - // TODO verify CA chain is fully present. - // TODO verify signing cert chains to Apple? - } - - problems -} - -fn verify_code_directory( - macho: &MachOBinary, - signature: &EmbeddedSignature, - cd: &CodeDirectoryBlob, - context: VerificationContext, -) -> Vec { - let mut problems = vec![]; - - match cd.digest_type { - DigestType::Sha256 | DigestType::Sha384 => {} - hash_type => problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::CodeDirectoryOldDigestAlgorithm(hash_type), - }), - } - - match macho.code_digests(cd.digest_type, cd.page_size as _) { - Ok(digests) => { - let mut cd_iter = cd.code_digests.iter().enumerate(); - let mut actual_iter = digests.iter().enumerate(); - - loop { - match (cd_iter.next(), actual_iter.next()) { - (None, None) => { - break; - } - (Some((cd_index, cd_digest)), Some((_, actual_digest))) => { - if &cd_digest.data != actual_digest { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::CodeDigestMismatch( - cd_index, - cd_digest.to_vec(), - actual_digest.clone(), - ), - }); - } - } - (None, Some((actual_index, actual_digest))) => { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::CodeDigestMissingEntry( - actual_index, - actual_digest.clone(), - ), - }); - } - (Some((cd_index, cd_digest)), None) => { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::CodeDigestExtraEntry( - cd_index, - cd_digest.to_vec(), - ), - }); - } - } - } - } - Err(e) => { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::CodeDigestError(e), - }); - } - } - - // All slots beneath some threshold should have a special hash. - // It isn't clear where this threshold is. But the alternate code directory and - // CMS slots appear to start at 0x1000. We set our limit at 32, which seems - // reasonable considering there are ~10 defined slots starting at value 0. - // - // The code directory doesn't have a digest because one cannot hash self. - for blob in &signature.blobs { - let slot = blob.slot; - - if u32::from(slot) < 32 - && !cd.slot_digests().contains_key(&slot) - && slot != CodeSigningSlot::CodeDirectory - { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::SlotDigestMissing(slot), - }); - } - } - - let max_slot = cd - .slot_digests() - .keys() - .map(|slot| u32::from(*slot)) - .filter(|slot| *slot < 32) - .max() - .unwrap_or(0); - - let null_digest = b"\0".repeat(cd.digest_size as usize); - - // Verify the special/slot digests we do have match reality. - for (slot, cd_digest) in cd.slot_digests().iter() { - match signature.find_slot(*slot) { - Some(entry) => match entry.digest_with(cd.digest_type) { - Ok(actual_digest) => { - if actual_digest != cd_digest.to_vec() { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::SlotDigestMismatch( - *slot, - cd_digest.to_vec(), - actual_digest, - ), - }); - } - } - Err(e) => { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::SlotDigestError(e), - }); - } - }, - None => { - // Some slots have external provided from somewhere that isn't a blob. - if slot.has_external_content() { - // TODO need to validate this external content somewhere. - } - // But slots with a null digest (all 0s) exist as placeholders when there - // is a higher numbered slot present. - else if u32::from(*slot) >= max_slot || cd_digest.to_vec() != null_digest { - problems.push(VerificationProblem { - context: context.clone(), - problem: VerificationProblemType::ExtraSlotDigest( - *slot, - cd_digest.to_vec(), - ), - }); - } - } - } - } - - // TODO verify code_limit[_64] is appropriate. - // TODO verify exec_seg_base is appropriate. - - problems -} diff --git a/apple-codesign/src/yubikey.rs b/apple-codesign/src/yubikey.rs deleted file mode 100644 index 7f9eb0b75..000000000 --- a/apple-codesign/src/yubikey.rs +++ /dev/null @@ -1,708 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Yubikey interaction. - -use { - crate::{ - cryptography::{rsa_oaep_post_decrypt_decode, PrivateKey}, - remote_signing::{session_negotiation::PublicKeyPeerDecrypt, RemoteSignError}, - AppleCodesignError, - }, - bcder::encode::Values, - bytes::Bytes, - log::{error, warn}, - signature::Signer, - std::ops::DerefMut, - std::sync::{Arc, Mutex, MutexGuard}, - x509::SubjectPublicKeyInfo, - x509_certificate::{ - asn1time, rfc3280, rfc5280, CapturedX509Certificate, EcdsaCurve, KeyAlgorithm, - KeyInfoSigner, Sign, Signature, SignatureAlgorithm, X509CertificateError, - }, - yubikey::{ - certificate::{CertInfo, Certificate as YkCertificate}, - piv::{import_ecc_key, import_rsa_key, AlgorithmId, SlotId}, - Error as YkError, MgmKey, YubiKey as RawYubiKey, {PinPolicy, TouchPolicy}, - }, - zeroize::Zeroizing, -}; - -/// A function that will attempt to resolve the PIN to unlock a YubiKey. -pub type PinCallback = fn() -> Result, AppleCodesignError>; - -fn algorithm_from_certificate( - cert: &CapturedX509Certificate, -) -> Result { - let key_algorithm = cert - .key_algorithm() - .ok_or(X509CertificateError::UnknownKeyAlgorithm(format!( - "{:?}", - cert.key_algorithm_oid() - )))?; - - match key_algorithm { - KeyAlgorithm::Rsa => match cert.rsa_public_key_data()?.modulus.as_slice().len() { - 129 => Ok(AlgorithmId::Rsa1024), - 257 => Ok(AlgorithmId::Rsa2048), - _ => Err(X509CertificateError::Other( - "unable to determine RSA key algorithm".into(), - )), - }, - KeyAlgorithm::Ed25519 => Err(X509CertificateError::UnknownKeyAlgorithm( - "unable to use ed25519 keys with smartcards".into(), - )), - KeyAlgorithm::Ecdsa(curve) => match curve { - EcdsaCurve::Secp256r1 => Ok(AlgorithmId::EccP256), - EcdsaCurve::Secp384r1 => Ok(AlgorithmId::EccP384), - }, - } -} - -/// Describes the needed authentication for an operation. -pub enum RequiredAuthentication { - Pin, - ManagementKey, - ManagementKeyAndPin, -} - -impl RequiredAuthentication { - pub fn requires_pin(&self) -> bool { - match self { - Self::Pin | Self::ManagementKeyAndPin => true, - Self::ManagementKey => false, - } - } - - pub fn requires_management_key(&self) -> bool { - match self { - Self::ManagementKey | Self::ManagementKeyAndPin => true, - Self::Pin => false, - } - } -} - -fn attempt_authenticated_operation( - yk: &mut RawYubiKey, - op: impl Fn(&mut RawYubiKey) -> Result, - required_authentication: RequiredAuthentication, - get_device_pin: Option<&PinCallback>, -) -> Result { - const MAX_ATTEMPTS: u8 = 3; - - for attempt in 1..MAX_ATTEMPTS + 1 { - warn!("attempt {}/{}", attempt, MAX_ATTEMPTS); - - match op(yk) { - Ok(x) => { - return Ok(x); - } - Err(AppleCodesignError::YubiKey(YkError::AuthenticationError)) => { - // This was our last attempt. Give up now. - if attempt == MAX_ATTEMPTS { - return Err(AppleCodesignError::SmartcardFailedAuthentication); - } - - warn!("device refused operation due to authentication error"); - - if required_authentication.requires_management_key() { - match yk.authenticate(MgmKey::default()) { - Ok(()) => { - warn!("management key authentication successful"); - } - Err(e) => { - error!("management key authentication failure: {}", e); - continue; - } - } - } - - if required_authentication.requires_pin() { - if let Some(pin_cb) = get_device_pin { - let pin = Zeroizing::new(pin_cb().map_err(|e| { - X509CertificateError::Other(format!( - "error retrieving device pin: {}", - e - )) - })?); - - match yk.verify_pin(&pin) { - Ok(()) => { - warn!("pin verification successful"); - } - Err(e) => { - error!("pin verification failure: {}", e); - continue; - } - } - } else { - warn!( - "unable to retrieve device pin; future attempts will fail; giving up" - ); - return Err(AppleCodesignError::SmartcardFailedAuthentication); - } - } - } - Err(e) => { - return Err(e); - } - } - } - - Err(AppleCodesignError::SmartcardFailedAuthentication) -} - -/// Represents a connection to a yubikey device. -pub struct YubiKey { - yk: Arc>, - pin_callback: Option, -} - -impl From for YubiKey { - fn from(yk: RawYubiKey) -> Self { - Self { - yk: Arc::new(Mutex::new(yk)), - pin_callback: None, - } - } -} - -impl YubiKey { - /// Construct a new instance. - pub fn new() -> Result { - let yk = Arc::new(Mutex::new(RawYubiKey::open()?)); - - Ok(Self { - yk, - pin_callback: None, - }) - } - - /// Set a callback function to be used for retrieving the PIN. - pub fn set_pin_callback(&mut self, cb: PinCallback) { - self.pin_callback = Some(cb); - } - - pub fn inner(&self) -> Result, AppleCodesignError> { - self.yk.lock().map_err(|_| AppleCodesignError::PoisonedLock) - } - - /// Find certificates in this device. - pub fn find_certificates( - &mut self, - ) -> Result, AppleCodesignError> { - let mut guard = self.inner()?; - let yk = guard.deref_mut(); - - let slots = yk - .piv_keys()? - .into_iter() - .map(|key| key.slot()) - .collect::>(); - - let mut res = vec![]; - - for slot in slots { - let cert = YkCertificate::read(yk, slot)?; - - let cert = CapturedX509Certificate::from_der(cert.into_buffer().to_vec())?; - - res.push((slot, cert)); - } - - Ok(res) - } - - /// Obtain an entity for creating signatures using a certificate at a slot. - pub fn get_certificate_signer( - &mut self, - slot_id: SlotId, - ) -> Result, AppleCodesignError> { - Ok(self - .find_certificates()? - .into_iter() - .find_map(|(slot, cert)| { - if slot == slot_id { - Some(CertificateSigner { - yk: self.yk.clone(), - slot: slot_id, - cert, - pin_callback: self.pin_callback.clone(), - }) - } else { - None - } - })) - } - - fn import_rsa_key( - &mut self, - p: &[u8], - q: &[u8], - cert: &CapturedX509Certificate, - slot: SlotId, - touch_policy: TouchPolicy, - pin_policy: PinPolicy, - ) -> Result<(), AppleCodesignError> { - let slot_pretty = hex::encode([u8::from(slot)]); - - let public_key_data = cert.rsa_public_key_data()?; - - let algorithm = match public_key_data.modulus.as_slice().len() { - 129 => AlgorithmId::Rsa1024, - 257 => AlgorithmId::Rsa2048, - _ => { - return Err(X509CertificateError::Other( - "unable to determine RSA key algorithm".into(), - ) - .into()); - } - }; - - warn!( - "attempting import of {:?} private key to slot {}", - algorithm, slot_pretty - ); - - let mut yk = self.inner()?; - - attempt_authenticated_operation( - yk.deref_mut(), - |yk| { - let rsa_key = ::yubikey::piv::RsaKeyData::new(&p, &q); - - import_rsa_key(yk, slot, algorithm, rsa_key, touch_policy, pin_policy)?; - - Ok(()) - }, - RequiredAuthentication::ManagementKeyAndPin, - self.pin_callback.as_ref(), - )?; - - Ok(()) - } - - fn import_ecdsa_key( - &mut self, - private_key: &[u8], - cert: &CapturedX509Certificate, - slot: SlotId, - touch_policy: TouchPolicy, - pin_policy: PinPolicy, - ) -> Result<(), AppleCodesignError> { - let slot_pretty = hex::encode([u8::from(slot)]); - - let algorithm = algorithm_from_certificate(cert)?; - - warn!( - "attempting import of ECDSA private key to slot {}", - slot_pretty - ); - - let mut yk = self.inner()?; - - attempt_authenticated_operation( - yk.deref_mut(), - |yk| { - import_ecc_key(yk, slot, algorithm, private_key, touch_policy, pin_policy)?; - - Ok(()) - }, - RequiredAuthentication::ManagementKeyAndPin, - self.pin_callback.as_ref(), - )?; - - Ok(()) - } - - /// Attempt to import a private key and certificate into the YubiKey. - pub fn import_key( - &mut self, - slot: SlotId, - key: &dyn KeyInfoSigner, - cert: &CapturedX509Certificate, - touch_policy: TouchPolicy, - pin_policy: PinPolicy, - ) -> Result<(), AppleCodesignError> { - let slot_pretty = hex::encode([u8::from(slot)]); - - match cert.key_algorithm() { - Some(KeyAlgorithm::Rsa) => { - let (p, q) = key.rsa_primes()?.ok_or_else(|| { - X509CertificateError::Other( - "could not locate RSA private key parameters".into(), - ) - })?; - - self.import_rsa_key(&p, &q, cert, slot, touch_policy, pin_policy)?; - } - Some(KeyAlgorithm::Ecdsa(_)) => { - let private_key = key.private_key_data().ok_or_else(|| { - X509CertificateError::Other("could not retrieve private key data".into()) - })?; - - self.import_ecdsa_key(&private_key, cert, slot, touch_policy, pin_policy)?; - } - Some(algorithm) => { - return Err(AppleCodesignError::CertificateUnsupportedKeyAlgorithm( - algorithm, - )); - } - None => { - return Err(X509CertificateError::UnknownKeyAlgorithm("unknown".into()).into()); - } - } - - warn!( - "successfully wrote private key to slot {}; proceeding to write certificate", - slot_pretty - ); - - // The key is imported! Now try to write the public certificate next to it. - self.import_certificate(slot, cert)?; - - warn!("successfully wrote certificate to slot {}", slot_pretty); - - Ok(()) - } - - /// Generate a new private key in the specified slot. - pub fn generate_key( - &mut self, - slot: SlotId, - touch_policy: TouchPolicy, - pin_policy: PinPolicy, - ) -> Result<(), AppleCodesignError> { - let slot_pretty = hex::encode([u8::from(slot)]); - - let mut yk = self.inner()?; - - // Apple seems to require RSA 2048 in their CSR requests. So hardcode until we - // have a reason to support others. - let algorithm = AlgorithmId::Rsa2048; - let key_algorithm = KeyAlgorithm::Rsa; - let signature_algorithm = SignatureAlgorithm::RsaSha256; - - // There's unfortunately some hackiness here. - // - // We don't have an API to access the public key info for a slot containing a private key - // and no certificate. In order to get around this limitation and allow our signer - // implementation to work (which is needed in order to issue a CSR with the new key), - // we import a fake certificate into the slot. The certificate has the signature and - // public key info of a "real" certificate. This enables us to sign using the private key. - // We don't even bother with self signing the certificate because we don't even want to - // give the illusion that the certificate is proper. - - warn!( - "attempting to generate {:?} key in slot {}", - algorithm, slot_pretty, - ); - - // Any existing certificate would stop working once its private key changes. - // So delete the certificate first to avoid false promises of a working certificate - // in the slot. - attempt_authenticated_operation( - yk.deref_mut(), - |yk| { - warn!("ensuring slot doesn't contain a certificate"); - Ok(YkCertificate::delete(yk, slot)?) - }, - RequiredAuthentication::ManagementKeyAndPin, - self.pin_callback.as_ref(), - )?; - - let key_info = attempt_authenticated_operation( - yk.deref_mut(), - |yk| { - warn!("generating new key on device..."); - Ok(yubikey::piv::generate( - yk, - slot, - algorithm, - pin_policy, - touch_policy, - )?) - }, - RequiredAuthentication::ManagementKeyAndPin, - self.pin_callback.as_ref(), - )?; - - warn!("private key successfully generated"); - - let mut subject = rfc3280::Name::default(); - subject - .append_common_name_utf8_string("unusable placeholder certificate") - .map_err(|e| AppleCodesignError::CertificateBuildError(format!("{:?}", e)))?; - - // We don't have an API to access the public key info for a slot containing a private key - // and no certificate. So we write a placeholder self-signed certificate to allow future - // operations to have access to public key metadata. - let tbs_certificate = rfc5280::TbsCertificate { - version: Some(rfc5280::Version::V3), - serial_number: 1.into(), - signature: signature_algorithm.into(), - issuer: subject.clone(), - validity: rfc5280::Validity { - not_before: asn1time::Time::UtcTime(asn1time::UtcTime::now()), - not_after: asn1time::Time::UtcTime(asn1time::UtcTime::now()), - }, - subject, - subject_public_key_info: rfc5280::SubjectPublicKeyInfo { - algorithm: key_algorithm.into(), - subject_public_key: bcder::BitString::new(0, key_info.public_key().into()), - }, - issuer_unique_id: None, - subject_unique_id: None, - extensions: None, - raw_data: None, - }; - - // It appears the hardware doesn't validate the signature. That makes things - // easier! - - let temp_cert = rfc5280::Certificate { - tbs_certificate, - signature_algorithm: signature_algorithm.into(), - signature: bcder::BitString::new(0, Bytes::new()), - }; - - let mut temp_cert_der = vec![]; - temp_cert - .encode_ref() - .write_encoded(bcder::Mode::Der, &mut temp_cert_der)?; - - let fake_cert = YkCertificate::from_bytes(temp_cert_der)?; - - attempt_authenticated_operation( - yk.deref_mut(), - |yk| { - warn!("writing temp cert"); - Ok(fake_cert.write(yk, slot, CertInfo::Uncompressed)?) - }, - RequiredAuthentication::ManagementKeyAndPin, - self.pin_callback.as_ref(), - )?; - - Ok(()) - } - - /// Import a certificate into a PIV slot. - /// - /// This imports the public certificate only: the existing private key is untouched. - /// - /// No validation that the certificate matches the existing key is performed. - pub fn import_certificate( - &mut self, - slot: SlotId, - cert: &CapturedX509Certificate, - ) -> Result<(), AppleCodesignError> { - let slot_pretty = hex::encode([u8::from(slot)]); - - let cert = YkCertificate::from_bytes(cert.encode_der()?)?; - - let mut yk = self.inner()?; - - attempt_authenticated_operation( - yk.deref_mut(), - |yk| { - warn!("writing certificate to slot {}", slot_pretty); - Ok(cert.write(yk, slot, CertInfo::Uncompressed)?) - }, - RequiredAuthentication::ManagementKeyAndPin, - self.pin_callback.as_ref(), - )?; - - warn!("certificate import successful"); - - Ok(()) - } -} - -/// Entity for creating signatures using a certificate in a given PIV slot. -/// -/// This needs to be its own type so we can implement [Sign]. -#[derive(Clone)] -pub struct CertificateSigner { - yk: Arc>, - slot: SlotId, - cert: CapturedX509Certificate, - pin_callback: Option, -} - -impl Signer for CertificateSigner { - fn try_sign(&self, message: &[u8]) -> Result { - let algorithm_id = - algorithm_from_certificate(&self.cert).map_err(signature::Error::from_source)?; - - let signature_algorithm = self - .cert - .signature_algorithm() - .ok_or(X509CertificateError::UnknownDigestAlgorithm( - "failed to resolve digest algorithm for certificate".into(), - )) - .map_err(signature::Error::from_source)?; - - // We need to feed the digest into the signing api, not the data to be - // digested. - let digest_algorithm = signature_algorithm - .digest_algorithm() - .ok_or(X509CertificateError::UnknownDigestAlgorithm( - "unable to resolve digest algorithm from signature algorithm".into(), - )) - .map_err(signature::Error::from_source)?; - - // Need to apply PKCS#1 padding for RSA. - let digest = match algorithm_id { - AlgorithmId::Rsa1024 => digest_algorithm - .rsa_pkcs1_encode(&message, 1024 / 8) - .map_err(signature::Error::from_source)?, - AlgorithmId::Rsa2048 => digest_algorithm - .rsa_pkcs1_encode(&message, 2048 / 8) - .map_err(signature::Error::from_source)?, - AlgorithmId::EccP256 => digest_algorithm.digest_data(&message), - AlgorithmId::EccP384 => digest_algorithm.digest_data(&message), - }; - - let mut guard = self - .yk - .lock() - .map_err(|_| signature::Error::from_source("unable to acquire lock on YubiKey"))?; - - let yk = guard.deref_mut(); - - warn!("initial signing attempt may fail if the certificate requires a pin to unlock"); - - attempt_authenticated_operation( - yk, - |yk| { - let signature = ::yubikey::piv::sign_data(yk, &digest, algorithm_id, self.slot) - .map_err(AppleCodesignError::YubiKey)?; - - Ok(Signature::from(signature.to_vec())) - }, - RequiredAuthentication::Pin, - self.pin_callback.as_ref(), - ) - .map_err(signature::Error::from_source) - } -} - -impl Sign for CertificateSigner { - fn sign(&self, message: &[u8]) -> Result<(Vec, SignatureAlgorithm), X509CertificateError> { - let algorithm = self.signature_algorithm()?; - - Ok((self.try_sign(message)?.into(), algorithm)) - } - - fn key_algorithm(&self) -> Option { - self.cert.key_algorithm() - } - - fn public_key_data(&self) -> Bytes { - self.cert.public_key_data() - } - - fn signature_algorithm(&self) -> Result { - Ok(self.cert.signature_algorithm().ok_or( - X509CertificateError::UnknownSignatureAlgorithm(format!( - "{:?}", - self.cert.signature_algorithm_oid() - )), - )?) - } - - fn private_key_data(&self) -> Option> { - // We never have access to private keys stored on hardware devices. - None - } - - fn rsa_primes(&self) -> Result, Vec)>, X509CertificateError> { - Ok(None) - } -} - -impl KeyInfoSigner for CertificateSigner {} - -impl PublicKeyPeerDecrypt for CertificateSigner { - fn decrypt(&self, ciphertext: &[u8]) -> Result, RemoteSignError> { - let mut guard = self - .yk - .lock() - .map_err(|_| RemoteSignError::Crypto("unable to acquire lock on YubiKey".into()))?; - - let yk = guard.deref_mut(); - - let algorithm_id = algorithm_from_certificate(&self.cert)?; - - // The YubiKey's decrypt primitive is super low level. So we need to undo OAEP - // padding on RSA keys first. - - attempt_authenticated_operation( - yk, - |yk| { - let plaintext = - ::yubikey::piv::decrypt_data(yk, ciphertext, algorithm_id, self.slot)?; - - let rsa_modulus_length = match algorithm_id { - AlgorithmId::Rsa1024 => Some(1024 / 8), - AlgorithmId::Rsa2048 => Some(2048 / 8), - AlgorithmId::EccP256 | AlgorithmId::EccP384 => None, - }; - - let plaintext = match algorithm_id { - // The YubiKey only does RSA decrypt without padding awareness. So we need to decode - // padding ourselves. - AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => { - let mut digest = sha2::Sha256::default(); - let mut mgf_digest = sha2::Sha256::default(); - - rsa_oaep_post_decrypt_decode( - rsa_modulus_length.unwrap(), - plaintext.to_vec(), - &mut digest, - &mut mgf_digest, - None, - ) - .map_err(|e| { - RemoteSignError::Crypto(format!("error during OAEP decoding: {}", e)) - })? - } - - AlgorithmId::EccP256 | AlgorithmId::EccP384 => plaintext.to_vec(), - }; - - Ok(plaintext) - }, - RequiredAuthentication::Pin, - self.pin_callback.as_ref(), - ) - .map_err(|e| RemoteSignError::Crypto(format!("failed to decrypt using YubiKey: {}", e))) - } -} - -impl PrivateKey for CertificateSigner { - fn as_key_info_signer(&self) -> &dyn KeyInfoSigner { - self - } - - fn to_public_key_peer_decrypt( - &self, - ) -> Result, AppleCodesignError> { - Ok(Box::new(self.clone())) - } - - fn finish(&self) -> Result<(), AppleCodesignError> { - Ok(()) - } -} - -impl CertificateSigner { - pub fn slot(&self) -> SlotId { - self.slot - } - - pub fn certificate(&self) -> &CapturedX509Certificate { - &self.cert - } -} diff --git a/apple-flat-package/Cargo.toml b/apple-flat-package/Cargo.toml deleted file mode 100644 index f7060e2e0..000000000 --- a/apple-flat-package/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "apple-flat-package" -version = "0.10.0-pre" -authors = ["Gregory Szorc "] -edition = "2021" -license = "MPL-2.0" -description = "Apple flat package (.pkg) format handling" -keywords = ["apple", "flat-package", "pkgbuild", "pkg", "productbuild"] -homepage = "https://github.com/indygreg/PyOxidizer" -repository = "https://github.com/indygreg/PyOxidizer.git" -readme = "README.md" - -[dependencies] -flate2 = "1.0" -scroll = { version ="0.11", features = ["derive"] } -serde-xml-rs = "0.5" -serde = { version = "1.0", features = ["derive"] } -thiserror = "1.0" - -[dependencies.apple-xar] -path = "../apple-xar" -version = "0.10.0-pre" - -[dependencies.cpio-archive] -path = "../cpio-archive" -version = "0.6.0-pre" diff --git a/apple-flat-package/README.md b/apple-flat-package/README.md deleted file mode 100644 index 919300675..000000000 --- a/apple-flat-package/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# apple-flat-package - -This crate implements an interface to Apple's *flat package* installer package -file format. This is the XAR-based installer package (`.pkg`) format used since -macOS 10.5. - -The interface is in pure Rust and doesn't require the use of Apple specific -tools or hardware to run. The functionality in this crate could be used to -reimplement Apple installer tools like `pkgbuild` and `productbuild`. diff --git a/apple-flat-package/src/component_package.rs b/apple-flat-package/src/component_package.rs deleted file mode 100644 index ee3b48bab..000000000 --- a/apple-flat-package/src/component_package.rs +++ /dev/null @@ -1,92 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Interface to component packages, installable units within flat packages. - -use { - crate::{package_info::PackageInfo, PkgResult}, - cpio_archive::ChainedCpioReader, - std::io::{Cursor, Read}, -}; - -const GZIP_HEADER: [u8; 3] = [0x1f, 0x8b, 0x08]; - -/// Attempt to decode the compressed content of an archive file. -/// -/// The content can be compressed with various formats. This attempts to -/// sniff them and apply an appropriate decompressor. -fn decode_archive(data: Vec) -> PkgResult> { - if data.len() > 3 && data[0..3] == GZIP_HEADER { - Ok(Box::new(flate2::read::GzDecoder::new(Cursor::new(data))) as Box) - } else { - Ok(Box::new(Cursor::new(data)) as Box) - } -} - -/// Type alias representing a generic reader for a cpio archive. -pub type CpioReader = Box>>; - -fn cpio_reader(data: &[u8]) -> PkgResult { - let decoder = decode_archive(data.to_vec())?; - Ok(cpio_archive::reader(decoder)?) -} - -/// Read-only interface for a single *component package*. -pub struct ComponentPackageReader { - bom: Option>, - package_info: Option, - payload: Option>, - scripts: Option>, -} - -impl ComponentPackageReader { - /// Construct an instance with raw file data backing different files. - pub fn from_file_data( - bom: Option>, - package_info: Option>, - payload: Option>, - scripts: Option>, - ) -> PkgResult { - let package_info = if let Some(data) = package_info { - Some(PackageInfo::from_reader(Cursor::new(data))?) - } else { - None - }; - - Ok(Self { - bom, - package_info, - payload, - scripts, - }) - } - - /// Obtained the contents of the `Bom` file. - pub fn bom(&self) -> Option<&[u8]> { - self.bom.as_ref().map(|x| x.as_ref()) - } - - /// Obtain the parsed `PackageInfo` XML file. - pub fn package_info(&self) -> Option<&PackageInfo> { - self.package_info.as_ref() - } - - /// Obtain a reader for the `Payload` cpio archive. - pub fn payload_reader(&self) -> PkgResult> { - if let Some(payload) = &self.payload { - Ok(Some(cpio_reader(payload)?)) - } else { - Ok(None) - } - } - - /// Obtain a reader for the `Scripts` cpio archive. - pub fn scripts_reader(&self) -> PkgResult> { - if let Some(data) = &self.scripts { - Ok(Some(cpio_reader(data)?)) - } else { - Ok(None) - } - } -} diff --git a/apple-flat-package/src/distribution.rs b/apple-flat-package/src/distribution.rs deleted file mode 100644 index 2ce47ca9e..000000000 --- a/apple-flat-package/src/distribution.rs +++ /dev/null @@ -1,334 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Distribution XML file format. -//! -//! See https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html -//! for Apple's documentation of this file format. - -use { - crate::PkgResult, - serde::{Deserialize, Serialize}, - std::io::Read, -}; - -/// Represents a distribution XML file. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename = "installer-gui-script", rename_all = "kebab-case")] -pub struct Distribution { - #[serde(rename = "minSpecVersion")] - pub min_spec_version: u8, - - // maxSpecVersion and verifiedSpecVersion are reserved attributes but not yet defined. - pub background: Option, - pub choice: Vec, - pub choices_outline: ChoicesOutline, - pub conclusion: Option, - pub domains: Option, - pub installation_check: Option, - pub license: Option, - #[serde(default)] - pub locator: Vec, - pub options: Option, - #[serde(default)] - pub pkg_ref: Vec, - pub product: Option, - pub readme: Option, - pub script: Option