From e744d0b6d2fb0848d39f9d4590f04793b126818a Mon Sep 17 00:00:00 2001 From: Gregory Szorc Date: Sun, 18 Sep 2022 11:42:52 -0700 Subject: [PATCH] global: move apple-bundles, apple-codesign, apple-flat-packages, and apple-xar out of repository The canonical home of these projects is now https://github.com/indygreg/apple-platform-rs. This commit deletes the crates from this repo and updates / removes various references to these projects as appropriate. As part of this, we transition crate dependencies to published versions of these crates. We just published new versions of these crates from their new home so project metadata in the latest published version is accurate and to pick up new crate versions for other crates recently moved out of this repository. This was necessary to avoid diamond dependency problems. --- .github/workflows/rcodesign.yml | 20 - .github/workflows/sphinx.yml | 1 - .gitignore | 2 - Cargo.lock | 147 +- Cargo.toml | 4 - Justfile | 64 - apple-bundles/Cargo.toml | 20 - apple-bundles/README.md | 13 - apple-bundles/src/directory_bundle.rs | 660 ---- apple-bundles/src/lib.rs | 30 - apple-bundles/src/macos_application_bundle.rs | 458 --- apple-codesign/CHANGELOG.rst | 357 -- apple-codesign/Cargo.toml | 108 - apple-codesign/README.md | 48 - apple-codesign/docs/Makefile | 20 - apple-codesign/docs/apple_codesign.rst | 67 - ...pple_codesign_actions_initiator_output.png | Bin 75422 -> 0 bytes .../apple_codesign_actions_signer_output.png | Bin 57410 -> 0 bytes .../docs/apple_codesign_actions_sjs_join.png | Bin 59881 -> 0 bytes .../apple_codesign_certificate_management.rst | 235 -- .../docs/apple_codesign_concepts.rst | 158 - ...le_codesign_custom_assessment_policies.rst | 160 - .../docs/apple_codesign_debugging.rst | 49 - .../docs/apple_codesign_gatekeeper.rst | 151 - .../docs/apple_codesign_getting_started.rst | 117 - apple-codesign/docs/apple_codesign_quirks.rst | 145 - .../docs/apple_codesign_rcodesign.rst | 86 - .../docs/apple_codesign_remote_signing.rst | 352 -- .../apple_codesign_remote_signing_design.rst | 195 - ...apple_codesign_remote_signing_protocol.rst | 836 ----- .../docs/apple_codesign_smartcard.rst | 161 - apple-codesign/docs/conf.py | 34 - apple-codesign/docs/index.rst | 8 - .../src/app_store_connect/api_token.rs | 153 - apple-codesign/src/app_store_connect/mod.rs | 200 -- .../src/app_store_connect/notary_api.rs | 226 -- .../src/apple-certs/AppleAAI2CA.cer | Bin 1052 -> 0 bytes apple-codesign/src/apple-certs/AppleAAICA.cer | Bin 1489 -> 0 bytes .../src/apple-certs/AppleAAICAG3.cer | Bin 754 -> 0 bytes .../AppleApplicationIntegrationCA5G1.cer | Bin 765 -> 0 bytes .../AppleComputerRootCertificate.cer | Bin 1470 -> 0 bytes .../src/apple-certs/AppleISTCA2G1.cer | Bin 1092 -> 0 bytes .../src/apple-certs/AppleISTCA8G1.cer | Bin 1216 -> 0 bytes .../apple-certs/AppleIncRootCertificate.cer | Bin 1215 -> 0 bytes .../src/apple-certs/AppleRootCA-G2.cer | Bin 1430 -> 0 bytes .../src/apple-certs/AppleRootCA-G3.cer | Bin 583 -> 0 bytes ...leSoftwareUpdateCertificationAuthority.cer | Bin 1136 -> 0 bytes .../src/apple-certs/AppleTimestampCA.cer | Bin 1456 -> 0 bytes .../src/apple-certs/AppleWWDRCA.cer | Bin 1062 -> 0 bytes .../src/apple-certs/AppleWWDRCAG2.cer | Bin 763 -> 0 bytes .../src/apple-certs/AppleWWDRCAG3.cer | Bin 1109 -> 0 bytes .../src/apple-certs/AppleWWDRCAG4.cer | Bin 1113 -> 0 bytes .../src/apple-certs/AppleWWDRCAG5.cer | Bin 1113 -> 0 bytes .../src/apple-certs/AppleWWDRCAG6.cer | Bin 794 -> 0 bytes apple-codesign/src/apple-certs/DevAuthCA.cer | Bin 1051 -> 0 bytes .../src/apple-certs/DeveloperIDCA.cer | Bin 1032 -> 0 bytes .../src/apple-certs/DeveloperIDG2CA.cer | Bin 1090 -> 0 bytes apple-codesign/src/apple_certificates.rs | 548 --- apple-codesign/src/bundle_signing.rs | 613 ---- apple-codesign/src/certificate.rs | 1765 --------- apple-codesign/src/code_directory.rs | 800 ----- apple-codesign/src/code_requirement.rs | 2015 ----------- apple-codesign/src/code_resources.rs | 1481 -------- apple-codesign/src/cryptography.rs | 744 ---- apple-codesign/src/dmg.rs | 418 --- apple-codesign/src/embedded_signature.rs | 1494 -------- .../src/embedded_signature_builder.rs | 342 -- apple-codesign/src/entitlements.rs | 556 --- apple-codesign/src/error.rs | 362 -- apple-codesign/src/lib.rs | 160 - apple-codesign/src/macho.rs | 858 ----- apple-codesign/src/macho_signing.rs | 664 ---- apple-codesign/src/macos.rs | 334 -- apple-codesign/src/main.rs | 3156 ----------------- apple-codesign/src/notarization.rs | 458 --- apple-codesign/src/policy.rs | 306 -- apple-codesign/src/reader.rs | 996 ------ apple-codesign/src/remote_signing/mod.rs | 1084 ------ .../src/remote_signing/session_negotiation.rs | 896 ----- apple-codesign/src/signing.rs | 229 -- apple-codesign/src/signing_settings.rs | 1246 ------- apple-codesign/src/specification.rs | 324 -- apple-codesign/src/stapling.rs | 334 -- .../testdata/apple-signed-3rd-party-mac.cer | Bin 1480 -> 0 bytes .../apple-signed-apple-development.cer | Bin 1484 -> 0 bytes .../apple-signed-apple-distribution.cer | Bin 1485 -> 0 bytes .../apple-signed-developer-id-application.cer | Bin 1450 -> 0 bytes .../apple-signed-developer-id-installer.cer | Bin 1449 -> 0 bytes apple-codesign/src/testdata/ed25519.pk8 | Bin 48 -> 0 bytes apple-codesign/src/testdata/rsa-2048.pk8 | Bin 1217 -> 0 bytes apple-codesign/src/testdata/secp256r1.pk8 | Bin 138 -> 0 bytes apple-codesign/src/ticket_lookup.rs | 248 -- apple-codesign/src/verify.rs | 549 --- apple-codesign/src/yubikey.rs | 708 ---- apple-flat-package/Cargo.toml | 26 - apple-flat-package/README.md | 9 - apple-flat-package/src/component_package.rs | 92 - apple-flat-package/src/distribution.rs | 334 -- apple-flat-package/src/lib.rs | 118 - apple-flat-package/src/package_info.rs | 228 -- apple-flat-package/src/reader.rs | 185 - apple-xar/Cargo.toml | 35 - apple-xar/README.md | 7 - apple-xar/src/format.rs | 82 - apple-xar/src/lib.rs | 60 - apple-xar/src/reader.rs | 394 -- apple-xar/src/signing.rs | 214 -- apple-xar/src/table_of_contents.rs | 690 ---- docs/conf.py | 2 - docs/index.rst | 12 - release/src/main.rs | 4 - tugger-code-signing/Cargo.toml | 10 +- .../src/apple-codesign-testuser.p12 | Bin tugger-code-signing/src/lib.rs | 3 +- tugger/Cargo.toml | 10 +- 115 files changed, 36 insertions(+), 31452 deletions(-) delete mode 100644 .github/workflows/rcodesign.yml delete mode 100644 apple-bundles/Cargo.toml delete mode 100644 apple-bundles/README.md delete mode 100644 apple-bundles/src/directory_bundle.rs delete mode 100644 apple-bundles/src/lib.rs delete mode 100644 apple-bundles/src/macos_application_bundle.rs delete mode 100644 apple-codesign/CHANGELOG.rst delete mode 100644 apple-codesign/Cargo.toml delete mode 100644 apple-codesign/README.md delete mode 100644 apple-codesign/docs/Makefile delete mode 100644 apple-codesign/docs/apple_codesign.rst delete mode 100755 apple-codesign/docs/apple_codesign_actions_initiator_output.png delete mode 100755 apple-codesign/docs/apple_codesign_actions_signer_output.png delete mode 100755 apple-codesign/docs/apple_codesign_actions_sjs_join.png delete mode 100644 apple-codesign/docs/apple_codesign_certificate_management.rst delete mode 100644 apple-codesign/docs/apple_codesign_concepts.rst delete mode 100644 apple-codesign/docs/apple_codesign_custom_assessment_policies.rst delete mode 100644 apple-codesign/docs/apple_codesign_debugging.rst delete mode 100644 apple-codesign/docs/apple_codesign_gatekeeper.rst delete mode 100644 apple-codesign/docs/apple_codesign_getting_started.rst delete mode 100644 apple-codesign/docs/apple_codesign_quirks.rst delete mode 100644 apple-codesign/docs/apple_codesign_rcodesign.rst delete mode 100644 apple-codesign/docs/apple_codesign_remote_signing.rst delete mode 100644 apple-codesign/docs/apple_codesign_remote_signing_design.rst delete mode 100644 apple-codesign/docs/apple_codesign_remote_signing_protocol.rst delete mode 100644 apple-codesign/docs/apple_codesign_smartcard.rst delete mode 100644 apple-codesign/docs/conf.py delete mode 100644 apple-codesign/docs/index.rst delete mode 100644 apple-codesign/src/app_store_connect/api_token.rs delete mode 100644 apple-codesign/src/app_store_connect/mod.rs delete mode 100644 apple-codesign/src/app_store_connect/notary_api.rs delete mode 100644 apple-codesign/src/apple-certs/AppleAAI2CA.cer delete mode 100644 apple-codesign/src/apple-certs/AppleAAICA.cer delete mode 100644 apple-codesign/src/apple-certs/AppleAAICAG3.cer delete mode 100644 apple-codesign/src/apple-certs/AppleApplicationIntegrationCA5G1.cer delete mode 100644 apple-codesign/src/apple-certs/AppleComputerRootCertificate.cer delete mode 100644 apple-codesign/src/apple-certs/AppleISTCA2G1.cer delete mode 100644 apple-codesign/src/apple-certs/AppleISTCA8G1.cer delete mode 100644 apple-codesign/src/apple-certs/AppleIncRootCertificate.cer delete mode 100644 apple-codesign/src/apple-certs/AppleRootCA-G2.cer delete mode 100644 apple-codesign/src/apple-certs/AppleRootCA-G3.cer delete mode 100644 apple-codesign/src/apple-certs/AppleSoftwareUpdateCertificationAuthority.cer delete mode 100644 apple-codesign/src/apple-certs/AppleTimestampCA.cer delete mode 100644 apple-codesign/src/apple-certs/AppleWWDRCA.cer delete mode 100644 apple-codesign/src/apple-certs/AppleWWDRCAG2.cer delete mode 100644 apple-codesign/src/apple-certs/AppleWWDRCAG3.cer delete mode 100644 apple-codesign/src/apple-certs/AppleWWDRCAG4.cer delete mode 100644 apple-codesign/src/apple-certs/AppleWWDRCAG5.cer delete mode 100644 apple-codesign/src/apple-certs/AppleWWDRCAG6.cer delete mode 100644 apple-codesign/src/apple-certs/DevAuthCA.cer delete mode 100644 apple-codesign/src/apple-certs/DeveloperIDCA.cer delete mode 100644 apple-codesign/src/apple-certs/DeveloperIDG2CA.cer delete mode 100644 apple-codesign/src/apple_certificates.rs delete mode 100644 apple-codesign/src/bundle_signing.rs delete mode 100644 apple-codesign/src/certificate.rs delete mode 100644 apple-codesign/src/code_directory.rs delete mode 100644 apple-codesign/src/code_requirement.rs delete mode 100644 apple-codesign/src/code_resources.rs delete mode 100644 apple-codesign/src/cryptography.rs delete mode 100644 apple-codesign/src/dmg.rs delete mode 100644 apple-codesign/src/embedded_signature.rs delete mode 100644 apple-codesign/src/embedded_signature_builder.rs delete mode 100644 apple-codesign/src/entitlements.rs delete mode 100644 apple-codesign/src/error.rs delete mode 100644 apple-codesign/src/lib.rs delete mode 100644 apple-codesign/src/macho.rs delete mode 100644 apple-codesign/src/macho_signing.rs delete mode 100644 apple-codesign/src/macos.rs delete mode 100644 apple-codesign/src/main.rs delete mode 100644 apple-codesign/src/notarization.rs delete mode 100644 apple-codesign/src/policy.rs delete mode 100644 apple-codesign/src/reader.rs delete mode 100644 apple-codesign/src/remote_signing/mod.rs delete mode 100644 apple-codesign/src/remote_signing/session_negotiation.rs delete mode 100644 apple-codesign/src/signing.rs delete mode 100644 apple-codesign/src/signing_settings.rs delete mode 100644 apple-codesign/src/specification.rs delete mode 100644 apple-codesign/src/stapling.rs delete mode 100644 apple-codesign/src/testdata/apple-signed-3rd-party-mac.cer delete mode 100644 apple-codesign/src/testdata/apple-signed-apple-development.cer delete mode 100644 apple-codesign/src/testdata/apple-signed-apple-distribution.cer delete mode 100644 apple-codesign/src/testdata/apple-signed-developer-id-application.cer delete mode 100644 apple-codesign/src/testdata/apple-signed-developer-id-installer.cer delete mode 100644 apple-codesign/src/testdata/ed25519.pk8 delete mode 100644 apple-codesign/src/testdata/rsa-2048.pk8 delete mode 100644 apple-codesign/src/testdata/secp256r1.pk8 delete mode 100644 apple-codesign/src/ticket_lookup.rs delete mode 100644 apple-codesign/src/verify.rs delete mode 100644 apple-codesign/src/yubikey.rs delete mode 100644 apple-flat-package/Cargo.toml delete mode 100644 apple-flat-package/README.md delete mode 100644 apple-flat-package/src/component_package.rs delete mode 100644 apple-flat-package/src/distribution.rs delete mode 100644 apple-flat-package/src/lib.rs delete mode 100644 apple-flat-package/src/package_info.rs delete mode 100644 apple-flat-package/src/reader.rs delete mode 100644 apple-xar/Cargo.toml delete mode 100644 apple-xar/README.md delete mode 100644 apple-xar/src/format.rs delete mode 100644 apple-xar/src/lib.rs delete mode 100644 apple-xar/src/reader.rs delete mode 100644 apple-xar/src/signing.rs delete mode 100644 apple-xar/src/table_of_contents.rs rename {apple-codesign => tugger-code-signing}/src/apple-codesign-testuser.p12 (100%) 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 692fd4ecf1a3a94678bf7b97b5322bbf5cfa1a23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75422 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 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 eb4ba58c05f966ccf171df69c242edb360812cf5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57410 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%0DFq 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 LXRKSKV;}rKo$J>2 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 83ac1be7f33ea96f00faf45606a834aef5e16b21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59881 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�s3)@Y!w{Fj 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 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 0038b604398a87ba272f592d32cce914b57ccb8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1052 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 diff --git a/apple-codesign/src/apple-certs/AppleISTCA2G1.cer b/apple-codesign/src/apple-certs/AppleISTCA2G1.cer deleted file mode 100644 index 46711ce49f99cb012c5141de95fb4a5592c817fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1092 zcmXqLVsS8NV%Au|%*4pV#LQ$>V!+GBsnzDu_MMlJk(-slz{!x?fRl|ml!Z;0DKywn z%s>Rh;S%O`Pt6Z0DlINi@XSlrGn6)v1c@^X3&F+RbMliCa}=B%4dldm4NVNp49yHp zO^ghTqQrTPj0{YT44_XsMBXPo_fZ~qJ5mygXF*#EdnXVtj2tqZ%ngtI=8 zH#@d2D7$vc3Ckm2(yhKXpOegQ)30-_bC*dEIyIFw_K{g&>wzoj4W{{`ckTYlUi#Z_ z5^&>9nby9?mv24!d*ZbQ(=@^9;)yaZk3?EK-7&hzDSK<#Md!Bvahvw^oKL&W*v+(V zipOV*{=ct`R&cy&n;tkt#m>`nTJVd?g$|7`S$9vK8u;h&-x@*h@AEh|i+YN(|Ga3v zV79l>x2;F+WL^Bj_4eC?qnZm=bQk{PowVm)NPz@b?(w%gzfbD29qwuUbGj|&npvMv zH)s8CNhyn~AEp>o@h|K7%*4#dz__@HQ5F~?k_Pg?(3DkXkuVTz5IImaBcr$T_iEM@ z9>LmGwW&K!o$)h}1u5WT5n~a#Q8mS-W!+8bgVT~8kY}LB#-Yu|$jZvj$jG8(07@EAz5!#K5Q+&YAQKFHK-MU*xEeSc zII?jiw0SVL{cvSuH#0CcFobDkViZ$_YAr7>*Go^$F99WOunUS(i}Op1l2eQIlM9NG zlMpa>0Fw|SgP419e(R#7htIXjc6Uy7Qk%ZVYt5R1U@LpWW?LJX)w3RLjFl;qT42Ld z6u$5BynqKMW?qZ9&h+XWuVzTi*{_F=P0$QooG-G1+lkj|MlM^$yboE6Wtq4ZX)NhI z)aYiHn||w_{l8W|CoQ3STett-A-#OsoAUW@t^`l5`p^F56PL(=z@UxS`U1G6X2$H{ zKDqbp+aGZU!*2W5DK8ORsB=Y{b=DKx6Gg-ZN74JzpG~ku9J%Bd+WEDesNPPe#nTV0L@j)HmB2 XpL{x-FJ0O0BVg0{#P?UoomI;Kn9Gbg diff --git a/apple-codesign/src/apple-certs/AppleISTCA8G1.cer b/apple-codesign/src/apple-certs/AppleISTCA8G1.cer deleted file mode 100644 index ecddd53ddf918da7b84ccd9bdc91a9f35a274f3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1216 zcmZ{iTWs4@7{}{N(>80zwv@I)tTjVr9aWNJyG@!(MYD5pE=?AvxfZl7wNoc)nxwW9 z#{o@i5fdP>33X$DgjCox@dDk7#HJyEMhC12@x)D(>kII}wVEnmoghv-v^{WMj=s<5 z_dkFC&j;p^Fqnf^7Z4bN;T?vM>76TIKk|L|T6uq|=f`h>Uf2BKG55zKD1w05=kaZz zMTbOCodJ%F;T@n|EA)moCRGTFB}v9SdCr8}fK}^nGk`vBA4ULR#qKzO88KHu#F8aB zT@W+X6pxjp6jsRRRNMjvG!!uy|09B8W;!XVm^~%RnaK>7lrx1qMs32fS`%Xk(B0Wq zV=cf2>TA5Sjl^r^5J3*w25sYD0>2mBQ&(;^P{m>{g?Yxpn4Q9gG#&|SCO+W9}% z;0CZ$JE3pYZ}Hln-W~@?0E4cn9u^F3rG`LF2;G1h!^hL-cNl)VZe0DMWy${2OQ{#~ z1D8HIy>fizvsd?6iEA^5e?9-l+I#%^W#-Vv4^0)qxc=c$Y~^P0PjUXfC!J^JpkXiv zS-`HEh1~`S`Uh<16& zbi!d7uDuz>rSMW z;HDb{-u%2B?Lga?ubz!Ja-To0yZqcUUoD*=*5A5FfgX*%AMFAiN89f{G#FzeH+q*| zLoyrCnZMuO8Ts_~8AgN#z{7y~l5AVK^zOX;^YGr-)l)YQlOHK3FZO}OwT*fC*_YpZ zO8n(|i(~b#*GtlkbGjqTFz{BF`wp7l@VJW6_V9AU$`=|GNCZCTFk zSKMsE5eEfkyIo=^HsaFLir?<10Yl*t-XX9tnhrZEzEFaW z1>=H}wB!mTCl!WNn^+>*Ff~aFfv9`T?_?ZQOrY7QK3WlB0oeWgO+mI z6~)7pj~0ZP^i=jhkYyc|639~ydWy=@ZX)Xss1sTH6d9KE2QqxT;*Hx9j-a0(93y=W zdwHm=hIucOmJ2!6I>~xNf%2f3k5>CF31(8NId&E;ikeQ1S_3})^h~lW@uf_K2=!-i znp8sBRIchx2=S`mC-X|0_4#t%Sf%K)dKh=9oW$j1MHm^;8@1&OI54|26!y40b}HlFYQsBg2!4D>>yS-j;I@c+L7YuChh5U7`KHvAiEsOqE5wMI&W5Px=0B&b;#hyADPKr1x`dQTTp(jgCTo z!8UtFgP!fq=lSQ_e%AKXkUH`2+}53ZH{)ckownU-we|}?AHyW>jf!G=C0A{DZzqYZ zUR*fIJvj8>dVR;uKYl+hIQwj|k87R0Pjj_t->jT;Rj-bAq&^<-@B zm%W!-{69S|b&uzbviZg$sSC@eoYZAvW@KPo+{9P~43RPeK43h`@-s62XJG-R8#V)e z5MLO?XEk63QUIB4HrbfL%co zBPgB8DzG#$asX{)0b&Md!c0zKWi)8~WT3^yq0I(NqwGwKVsaTJB?ZM+`ugSN<$8&r zl&P1TpQ{gMB`4||G#-X4W-@5pCe^q(C^aWDF)uk)0hmHdGBS%5lHrLqRUxTTAu+E~ zp&+rS1js5bF3n9XR!B@vPAw>b=t%?WNd@6N1&|%Uq@D!K48=g%l*FPGg_6{wT%d-$ z6ouscyp&8(HYirePg5u@PSruNs30Gx7i1YwCER{crYR^&OfJa;IuB@ONosCtUP-YY za{2^jO7!e*{cX?eJDxY@8r; zW8atJ+3zl;@Sm>qH@UIM?q|jS>=W#7YAu_)gB31Y9ND;kmOoeaf9*e!%UL;V#2vx} zUUwk!R<>!CBEM2a=7;zgw~E zguTASugG_6SFxo3)|+Pa2irq$E}yy6$m#cutA+FG76xsX-aFYzMMzw9>OIdRD+ Xyc@&=R&`yy_2kb5PImJRrKO4hMS!&; 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 739b8141312801fc4e88396bf4366075e2711173..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1430 zcmXqLVx45r#9Xz2nTe5!iG%UM)2+?viys*9vTOAw3^$fWUI6;DJ!c3vT26E!Oh9(9k#s-EKh6V@Moh?3yZ7ptT%{oZtmbE?IX_gbDkv37-N%oe}J zzP5HeiJHX2V)W^R!|$N@`QHwFe7wtbLBLg^?oW5`O>9&C@O4{}9^)$g`tk5`oHG2U-jvVF(JFAtQy>@PC8|Gm+-ur_evqs<>KYls!+pFOkI zyw>maT8D@Bjeoz}a@j5VxYux+X5^hik_|o4CC79w3k0jliufP7(wJt`o^Grz!truT zd5cLk8RGj7?_MQ5Sw1#If5q{d-@GFGL~T_}KFzvQ>X@dqh4C8q`U&%RUM2mQ z9cjbyXqw5BL}ue|2zV0PvXSiivJZh)`o?5)W*}w22NK{1 z39tY&9UF2g175L?oeT7jmWF>&3sQpoaUAA37lOmwM#Iu zvEs(Zt*#Cgt5+O2_q#UPJmcH*-|LSZ_I@L8rY0QHHl=9OE2g*IxleeOO*jx$J&Y#WKm>kMqyI%SzoNwLx|7a?QVA>=u=J?w@!2U}iCt+`i2!(e>BKPs%5fi}#0h8g95*leD~AbVWzkY72)$XN3NX zKK@{S-@NGbnHkUaFWU19&c)8#u)Swc2lF=;T`x`$uV~zj- 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 228bfa39cbd5acfe53fb9d196e3c1bbbd28649f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 583 zcmXqLVsbWUVm!HknTe5!i9`43pN>mMy{8&*v2kd%d7QIlVP-Z+HbQ8gAnH8xlJyL^4LCu9Y{E>T!3J{TyoM$QCdLMa7KWAvW>Mn2#+FE2`Z=V` zK!A-M?0+UkHdgIM76v8eBnFllFZFF5ik7^ctW?w}EOS?2>c^vt{R;1hh~4CSx{Ot; zJf%9`&*JiK8JDf~U*)$MCB>e6*%Iw<;4c`(@HZlYXX#gd9ba~L;nG{vr%%r}jCrd) zw_3sa#?FwNaWj`#1#%fKb~11^i`@ju(^Y`3K= md8u*U-Ks5L%FuSdyxM%SHtQIk3xk#Br&Bd58zu zL&#n>Xkt`C4irXK2IeM4eg>d87gG}>Bg2*t3Q}8y*M#j?ikIYSi92{yhGiaWdUW3W z*|EzvKi2vEe?payneYnX(rI#)XD>6(4fj7B!NT}9J8#$eC(q9>RWF*dy~M|Nn}yTC zZjap}l{XGW{p~yZAwb5m?9o)=0QW!7v{rPVy)Y}nKIY!!iK!kRe}4^ieYr#7YPj{} zEe3*13v)O^Hr}XQDWiPv{nu-$=U%QaUb|7p*+-ZA;kol-cem@k3FNTQ*ynmP&gbSP zt(v==BrZQXx6A46!;+vLM<>?zZ}#Us-aPYZoT6LEmOn)^eN*TEV!svL^ZZQC!|Kon z%fk0KeV@Ibt>FbvQJ&DoHgB=&>WZnxi*_#9SGp+9g-7bwTqb5l2FArrj7$cN{|)$o z!7VGy$oQXy)qojD8OVbKlvyMU#2Q4jFJ!t;DdYV2CA(M8VcB1&V-aC^-?Fuw+$P zcn!Fr$u~KLh0#C{WVt+xx`C>J@&d&L@@=wE1Cxt#kjw*_p$8N)kOisdV-aH!i46a< z%kuo?h%#s4%_okqt>p;Tb}`@sN%Mm|#{$eeYz6{sT;QydoWjN=%)$iBEvT6Xm`)fO zCS_KzG;efi;<&x>H_xraNA}Wr-&xkRR`ChBafPI4c+{6!?G!cXq{H?Ku+gREX%`w+Em=|L8zFGmdo0y00P`nReW zHuIbn{MlD$T6O2T()o(NLUvm!+}H{O@|vf_FnxEGTkv2KZ-irB!$O_V&q^<~mnYsi Pwk|5ixBufCo28WitgNNX diff --git a/apple-codesign/src/apple-certs/AppleTimestampCA.cer b/apple-codesign/src/apple-certs/AppleTimestampCA.cer deleted file mode 100644 index dc0538f00e7491eced50d968b025999917b7c252..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1456 zcmXqLVqrIEVtT)TnTe5!iKEshJbC`-58DlR**LY@JlekVGBR?rG8iNoavN~6F^96S z2{VNT8wwi;f;e2lT#f|=IjIVsdC7W)Y6dDGL2hALh@f+7QAuW6W^!UlW`3T6V`)i7 zeoAC<|amd2B0_>QxhX2 z!{u^`h2Liiq{m#+o42E1IodS5?u z^7h=Z{l<1azfIML^N{ojzKV|CgGxJgDGJX%^{#8)-RkK7i(g(`+*tST|Fa&?C)Qj) z1(p>V@76iu6;;G#8BlUpBd;>PZutt4^I@CAa+bdQaAe)jR|fJx3fk+GXWg z)D2V(lou#2kZ+UCC@Cqh($`Ne%F#;%B|*L9{9OGaU`o;h3K{T$4CDvdzyeIyZ3Y5t zT;SB4oWjP$$-)Fo(5UGf7?q3+W|zL+D-32oseGeicIuwbSEFC=WDi3fzx-bmF=5!skHlW?3}Nsd+uaqS>1gdQu|gU`|~xs$tQBF zLgGI;_X%7mi1?ytcSMaQvEtk6aF~5P6$;)+@ z880ut>r`A?6X@JJD?#ASI`vE0=_id{^H0u@TQIBMS8(#f%>Qq^T|fJsQsN80uEhRS zX-oUdIhS2e=h;8)+wsTkYRt(Et68mb%{3OUa7)b(U6|4FD#-5Smn+YH*)Msb*09wo z``Y2BKaCns7%lR6*vGb5^2z4Y*53CYoZ;KFe#XUS=gdO)V9zAuNTWb^$MkGh$MjG~ zUq=IX=U|99mrJ0llXH2fqoawZlVhNxkwIBRYI2ybM`dt8WU5z5PFX-eSh#ksNm@m5 zR)v{QK#6yDzFAsUO0s8qhJRW>5|^)Ol}mDVKzKl?S5S_5VTnhiS+GG~xKFvEQL<%n zsjsnnL7{(%aZ#9uafzFIS(RaySxBY3p;?%ler2GENg$VjM^tidxudT~sJVB5ud$hF zS(eS-NkLS8-}oQb3kVm6Nlde{QKkxkpHWrJ*xdvT>M2 zXsEwiZmwgXvA0Q{L2gN2Wr&|!l0|N$N1=0Knt?%3wzgY=zIKIAP@12AWu~Wpm}PRN zlXhrkiDy~?SDv4rTcweSzGr&*4t zc4b;fsA*Zget~~rZn{sXS)yf<3zunHc4c~UdS!lgRz_BOVwQVqa8+ehiI1_ii?2(O fcUi8Vy9<{t2)O#WC^)+Yg?PGoIy;8Ag2lK1JdPLN diff --git a/apple-codesign/src/apple-certs/AppleWWDRCA.cer b/apple-codesign/src/apple-certs/AppleWWDRCA.cer deleted file mode 100644 index d2bb1da64122c864c872d9b711b176d042462748..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1062 zcmXqLVo@?^V&+=F%*4pV#KCxP&k@Vq1p)@VY@Awc9&O)w85vnw84QvPxeYkkm_u3E zgqcEv4TTK^K^!h&F2{m`oKywRyktE?H3JopAh)nAM9?|4s3bEjGdZy&Ge1wkv9u&3 zzbLb$(ooDm1f-5xm=~fhC_leM!P(J3PMp`!*ucoZ+{nZl`k>Qj@$g1r8WvHYTX!VypjWG%{)c-lQVYXRuCJE(pY=;r$WxRF4-br${9`c}_k`!An>Li0EopDe zv*^E}E$UaTdLwGTU-QAd$KGuPNJ^ruh+PumVL-h)reU3&vOc4_NAnbP|3 z#gVzkGL~*w{3pGxU>8%Qce&F<%bj1(KJXzgbzkmy4MuTzafLDk420{ zq(fbtbLBRPgzh)5cYSk@JQ@_Tc)I~VNLrYY@jnZz0W**?kOv7Uvq%_-HHc_m$aJ4l z#`*6{cCVhpvhVJ`^&D{qdLRYzEb0cT2FeQ*7s$8CW|Wi^Sn2C07v<Nhma(=FU z5ipVI0fh|sKiL>2Sz0$ga7&Wk^6MMZpzW` ze<$2-^n%rNMP7I9$xNP|H^ujq>s(2H^mkUSRbjJu1>%3p6qBF+hu?6 zedkq{<@Qo*x8F5!lFt%JA<5kEuT>H4)frt++IqZRKk^h=we)T%!^(BLy$#kqT(EJE zX2Ubi@~8Vu7BQZxzw?Oene~p{Z+0b3{mh!|*mRcPTGnUklH03)o}Bv9|B3H&wV91C z_x#+Vd5N(q?V(=JH^r`_KPnzJuG@ck!rYZ>Kd=95AvG=CKqhc$%$vflrY$-AJfiXd D2`7-6 diff --git a/apple-codesign/src/apple-certs/AppleWWDRCAG2.cer b/apple-codesign/src/apple-certs/AppleWWDRCAG2.cer deleted file mode 100644 index b77e1e9eb6eea7cf06c82cb37dc6134902970659..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 763 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 LuhX4VPq+dA`9+>| diff --git a/apple-codesign/src/apple-certs/AppleWWDRCAG6.cer b/apple-codesign/src/apple-certs/AppleWWDRCAG6.cer deleted file mode 100644 index 424a70bd3b78bd3ce45167b12673d056db250ef2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 794 zcmXqLViq%KVw$snnTe5!Nkr-3LU*o`;>`EKZ9*rH&9}AqoO9iPi;Y98&EuRc3p2Ap zx}mgzBpY)m3p0<9V?jYqszOkHeu;v!qk^u2yRo5~feJ{Hn@1KR>6}_rl9`s7oLG{X zpQqqhT9T1plvz?~C~P1IQpd%^1ySdjm#k;VZNLc8)CH8ulsEe*gR%AnNH#lQ*T5Nn7T;rT^5Ddm|dsR}NsWvMy&1*t^}L8&=ltBVzI zxddt{HxHA$84=DZGZ0{72Zsg|BO9xBBMXBPa}onf-VrYDd;IG{m!(!-Z@N=l8sz=a zR!Fk*0k7c7d7R(P7hnD`nI(Utb9dXp89$un&i%h>UBCEuwt}VCHtfr{x<0X8sp7fp z0gr!oEaYcp^_SG1D)jn)Bqi*52iw`ki3^paJQg?pGHCp6AO!TVtS}?ve-;h{HXy~s z$Y3B35>sZ8Fc51H*}dW3!U*GzSxdTR-rKkL_4-?DawJD15VD;uzkCs=IQyR6Ov{c@PVZH zL7rj(CPFq~0CNEYxb1s#3L6t63ll2?asp(|U@%B!GH{3!S@oo+YY{V(-5rbX4pHpu zeseBlQ>~uwAb(`##JMk27I$d-&3i1ITN*xFCg$i*CPRkH9)F_NPJM5-(=fQ?Nx&iV m`#SGMLj(PL3?nA&{ocH$^jO7D!QPDxpWJ7tmF-pM$p8TIXaMy9 diff --git a/apple-codesign/src/apple-certs/DevAuthCA.cer b/apple-codesign/src/apple-certs/DevAuthCA.cer deleted file mode 100644 index 3d8fb276401a365b90012ec5d9a2d57d95151b9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1051 zcmXqLVi7lJV*0;;nTe5!i6dRpaqp8$OQj5W**LY@JlekVGBR?rG8iNoavN~6F^96S z2{VNT8wwi;f;e2lT#f|=IjIVsdC7W)Y6dDGL2hALh@f+7QAuW6W^!UlW`3T6V`)i7 zeoTEBc`t0jqG55adwjQnNZx$y^{CiMw!5;GmH@R~5mRA-h3Hw(HfCT(N-krl~Rsm7^aeIqjgvf-v zUFf+vSID%f>UziuZ=*w+Tonv^E^5Y~OA~mo+&tMymFvl!4$DF&W=00a#f{4h8W$VL z0z+Pwk420{#PgdO=MHJ*U(C_lUNtSa87m->&St<5k``uU{LjK_zzn1efJx82@9!P;ai@JfTf${>y1@djO86_nJR{HwMMLBwj zpd_i6oS&;-1WZ|aKp_J@ka~WQ4J^QP&SoIM#syB@$ti41+$>DM1df{CflE}Ks{bjfcIXS|(xUZNx5`ZB42O+udQZuEJGtcekLaNg~Qo=H~Q)w{tL9AED| z?418ORoUy>Lwr@80^) z^2^Orwfkxsc5AJitadWVt%EUC>*kiQ@b}IylU}_3d;J4zAD7)ui64ePj85<{d^=H; zcg5awr&jaR178_5GHt!?HF`e^I`itGP3M~($GiJKTr<12F6^G=M$KEnF&ZJKl-wt! LpXU%x`}Pa~3t@{q diff --git a/apple-codesign/src/apple-certs/DeveloperIDCA.cer b/apple-codesign/src/apple-certs/DeveloperIDCA.cer deleted file mode 100644 index d3337393b5ba6a1cf74377a707350da14c3d5215..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1032 zcmXqLVqr08VtTWHnTe5!i9@1l<%&bo6nPAI**LY@JlekVGBR?rG8iNoavN~6F^96S z2{VNT8wwi;f;e2lT#f|=IjIVsdC7W)Y6dDGL2hALh@f+7QAuW6W^!UlW`3T6V`)i7 zeo0 zbl-oSljF(%uo;@7&!^1YX}Xx}?3M!$A0>P_^+vP8H8V3IJHBhupLxp9tPUw{HuRU- z^=X?)q|>_%E18%jCbDeXT%l(y-{8vYn!lSbD!Sh4`3;Wi+eeHr^(@8)6k6|SjZgP01 zRV-S)X8EH_KJAewnV1K6f%kseUUfDfdeA7ldyFj2P| z2(WR1lX7wj8xtD~6EHQSCT?ImSiuyb8Jb!xmM3+SuU47Q+4Y1MDAFV_?vyg zB%_^I_FEqeu*`BPS@b4azToMqyWTl6%7wb|7mrTf_ilDqL2A=nvBy#Z;+?uH&i>`8 zi16gms5gGcP$j*kY|XZ7vD zgfB}yma`q&9rZ|F-p#e{$@H)40^1doit>wY9SbNuYOC5GyO`nSe6t=8%l^&QuO>9; zO`VnSrQ+NdKCWQ9MZf%S$gOLc+H15Y%(wGpg~Wq3vz&HSxkO3d%qT^yj_qTOn kM|4=VT-S5o^j^utZ?dN2cw%U$%SrWO_IFKB{k|3e0G_UPUH||9 diff --git a/apple-codesign/src/apple-certs/DeveloperIDG2CA.cer b/apple-codesign/src/apple-certs/DeveloperIDG2CA.cer deleted file mode 100644 index 8cbcf6f46ce8dcd0fb6e55441867a4608c032860..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1090 zcmXqLVzD!5Vpdzg%*4pVBvQYH!T#)Y&#KeSzLS=8RTLj;iFG#MW#iOp^Jx3d%gD&h z%3zRW$Zf#M#vIDRCd?EXY$$9X2;y)Fb2%0iSS&nCU+yalX;MxjO;0c zCPpP>Z!@woFgG#sGXTZ8n3@lXWlhc{<6@_ zAeVa$Zs~tCyBS(Lx(wQVee%4v9DmC6_sxzm_*Wacv6#I#)Z>*hPs%}3Q09g3DcH!m}M zzME}-$);}wYkbz|o;?}o+I{{v$FroX4pX<9$DEPod-(p&(x<&A1GYQN=TcU6b?Bdx zUj1OoE6d9jOHD2wI{m=7UT>Su;iqa>RnM*J)hhopPh{Hk2Vdj$KD}68xZHa2OrI*H z#lG(g1g~CeYi#?H7fwq1S7-2zs_8d`#)~n?ko^>O~S}P zBuwq0phvQHX3=yH`M&5=acZK!OOzXoLyRRCDz!!3*x{YIYWcZo(++VRH_f_`f37RD zIVso6^6S55-|LI6bjZK$IKcnor?K1?f$fK41kGK{SvD+jncI`WTU{$#cV_G457+W- zMXMT?mRx?=Uwf(Jh2ioUN9FH7YKmOf(3s#R_GaGBE{9dpS`QQ3xOxisZ+hvx@ma(w zc&)N$X|k%KGE@HK=&108*Ije(pZQ)tKmPXdw<*i>UG(#PZ7V9c!nRmnw>qJnx@hvN w+Yzt&uIxN=z~GOOl<%%dJSvtmnyePgy!ZUclRbQUTGvOdj=AvX_8Ec00Cez?EdT%j 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 fe5bf80cff3c4e229641ec9994798d60c980a790..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1480 zcmY*ZX;2eq7|w1EA%~ozMZgug3J70+> z?z%0ceEYLV9DVp-9XO{F`_dNRtSKp`Y zPJG_AVsYxr$csLoB^ow9{yMPdVfCgnK^6WY`~D~ox1{iU1$uP;BAwc$=)eA!D_=J5 z+@1Jp-nolUQXblGhHiM1IlUvhcIf$?SEcK?`i~CR&j|0o3|~lEy7uXV;^Aad)9uh- zi?(|lwGZsO+UkC&tLxh4YyTFAj~p{@*0se|uMZpiAl>YzZvN9Rd{4joaC)e8sINr% z+V4&C58Y=!Z(F&gJ?mN9s_JPzvyYH(S*9WWY1q{}kM>_^Kej2mSJCB?f4}Xm{`X4X zl1tIB7!pA>RyYV)VFW07LH)fT$S#3_2uu^u49OJWQ|j$Hd~(v(*R{BOn`cW%7^d9$ z;Kv3)%UmL4kW@MWf+RT5ftgMWVB2&($I%JV(HK97Y!oVtY!pVBqRnQU$&ZP`2?nS~ z!y#MnyKo9)=`qm)1TixmNnj5DZ2hyOsm@McMEPSB~DBkT5441e9f8k=H#7E-I zs<6Jl3!1<03F;Y?Bvifd{%Y!ZFj2U66Y?ZtctR=glF3K*4~EMimywC!w~$k7$5P^* z1c}hP6QyCrq7!CwtlUi-|H&+mxSywf*LeB`9e4JqBI4lV({+_zf31Ef&R$!y!C>s2 zf3bJaqrJY$qFLv8*YlYvp#1Tv+b8SdD>Bk{F`cGH+xoz)_0g7X#|t&e1BtCodvkMr zOLX+al+_J8&d=SoCb6Wezo**mZPh^3J<-bU_MF}A==Gl}Q?iN7T?q<5Y6Q0guHt))Tig1NEBcj!)b-I$+MTI{x~llkUSc|e_bAYzX^*7U@j<1MXQLsuKsg-sr^)YIvoz}4^wzUpp(eY6PozgpFDE_fC`|bW7-+sH_ z0*~|}@JL$rh+znZN3E$SJK<}4*!%gj&0){Enj8=&lTY!^_4bEBkWwlFF3bex$ixK_ zF*1EN!`LZwG0ob^N*hU`CaQw6(+tI;d6XUJY_!9PW>YL@E3px;AUfN{mC>w?tHP8( zE>uw>aIPMU0+9kxAc{cAc@9DwjF3ga1^HlHbeIkU2AvVh)a%l8g*?;^L#z;S#onRBQOLwrvw_jW?D`;xM^rEOO?`W6`KDs%@Sym z$&~hi<^2VQ4AbNwF(sm~m~}c!;eyPJbW9CY0k&apWWGt*15Fqy9V8WrJy3)QcpwGr zfgsV*x-nAc^zy&k&(6A4)V8kWUihjkbM(%fe>Blarb*b-`fVRRaCQ3Kjf0Qhadl zp3;5OV>T7lU+wuzTIcFR?yY3we=zRqD%>*X)Z~h5$zvB@`X%;g+Y3GD${&Yb->~}1 z^6{-p&DrLgzMQQy_n+e?)@)oMx~M!9J7Mi3(({uH?Xa#ta63O4Rqox@yr}!RW6@db z^&2yv!D2`R?eoAHzyliq!V4M~1wlg+7>K}l0Zo*|0ClZO{_puQbpyvq#g@_Snj`{g zdT{6@U}T}mGDs>7fglM9tRN+b0ql)0<2YtoYAV4$Jf{{Ho~V|f%Tp^WNw!q4B`FqQ z!{LxO;dMAoIGGXA0tEV$KoT(U*EUWdg-Sfo7#>9OBWMhQltREg1l*m$vM>zXUn1^S z-UT+?K~N~|K)EuT6BRfG%s`>mDin8Ap$yJ){LJZel~Ya>CkTq+0y!mo4kZi+Dx8A1 zkd=3vliwwAmP9#I4vsRdCo#jEdzAY~Bz>Ot_I&d5b1xERzw z@G%f*JI=PdfMW%U#!`KQ2=pSCx3<>+hn=nU8!!fW6yxx}tNfVf**Jy;GWf1M?dn zVPrXwu3Z9Ot~jCzy<#646wVZmBv}bjXJ*TGcHrCKleWrVRsN|FP zPDWkI{%zGY_56g$#KaeQ`VH}_EKfj!5Gz(RRdRk zFCM+Qdg0;BhPKcZkK`$vti}6lKCSYfc{~1_-O;ud-_84R6GZRUmOixjylEk)z18?1 L%t_5bV`BGzphyBa 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 f3a9e317acfd3de23f3789e49836f43f7748d2a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1485 zcmY*Zc~BEq9M0PukV~$RB39Yrpa>XV5($Vx{O)KQc*|$u%p&?iWMtbnzk@i?5=4L{@9uQ-uoTj``+(+ zkQI+YR@C1vL;!+#rcAZzM<$e6_I2a3O9(WFq)KqR87I(0qKqgZtptT;5G6*&Oj>9(fuI<3p&5G#(Ks7ZOj2g1Tq}oC zzKUFdY|(3dpcfBh3YoKHvIWz)A|yUYb|zf$no_5Q>vRU~dc7_pDvyJ@c^H`os}5-W zp`S!1ZCO=+a5@YJKp{&@)#=jm($_~tY9nEUle5OzAkj=Sl-XqCIYy&fD58j@%F)bS zB!!{DukzNon8_DaQ$v-Lu5wZ4V#X{8T&Nuo3xq825I`2l5EcMI&(rVtbtYzbkwroFNM~LBC-1Tdl zD=K@XdTk8-;M5dr}?#UfFVMb<$D zC+Kn?0A7d?D1ZSxS}F2_>PnS#?xJtgY#%Ob@g7_ij42NPer5n#CGMdTAQrm;Ai|*u zu61FE)V@*7FxKesaEzNgT4UsARD+SF;dVPt73noNK|$?&IH(Q!Kb*v9>w;(=Lj782 z5{%%kZ3RyX6|umV176(cat4Nr0jzoftG;oSMG#o^nWE|(X9Kg*f)OZbL78GRjq;qF zZXi%oIcl_&qgEruaDOyyDuh+%}4apn|qIRyV%Q2rF0hfJK^Xl@rbQaH*GR4JEW z!BLE~;AW@Y$U@Xc6Jd)QSxGLJZ^lK=_vZ#5wNgfmF>~~|nha4&SUA04^;Dygotv6)K?2RYWL+%lY*sBDF~6{%P_}aiFw$^F;1IW%$9} zJ8$YBT*7aA@M$6;;#LWLB(j(94TehqcfH&|aIen7MUiXKrN8rK;@kxUZ_kt+CsE17 zjt{$(6T7`9;=>ACWA2Y#!FL>~nHm21et(;^TRu9Nagg?F-I(>D+%&nrRp>}Ruh)uaWg6Rd)7y_{c%Jv?8pd;FN-)eK6N8zENRb$n0K zkXO_61+lDARCJ=D?#SiPr$x_y*?a6#bzSDmpLQyT8@Vj1)(zJ+m6RRgFWp w^^b-PKRiC2(=M6vyy|^md6}kQwC_9}`;kUE%cgoXc47Werueh+0MncQ0T`SD-T(jq 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 dca1bfc0dcdb695a68243b560ca34dbe971148a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1450 zcmZ`(dr%a09KYY*L%G`n;Z7bVvgiqjJbrf|0@fsn0-+9plo2(YdkdT#x98sCaV5%K zief%^4u^>m81WjZcm!IQJ)EQ|5-5j)1Rtr53}zT_XqSh=AMMOjm6c9v78`68i}asUs*X zA{5&~P_#NxjVoz2sl`Il=wy;o(?+=)l(FU_)Hy__(-8k_CxN!Coe&ky+C^w_AGrux zv6lh~5*q_oIfxZ<=&w*fISlku#4#{{fqpYk9tTZ@@)gigAhgPJ%g%r7r2+sI?ub+< zcEm*m1#XnT05{F*_Ftr%jW(w`7z?9_B$6^>F>jL;j(I#|uanQ|6D{&-52drB#fzAW zb(_KJg;@lA4%ZB9IM57)h#3G*eMx@e-HGY0!uX$49>kAY z3$_ji?fdr_b>|Me%bra zvBqh)7t@DI&PJnUJt>zX$QDa)L00Hr_dV?1I2Y;qBni;HFX#M3rCofuK`f(0+ql2I z^&olB>`@(tj7!1=@u!cB)u0E}yXDzOU5etzN}_(b{QAWakIjLa%FePX$BC`!w+~sL z8|r_FI^=oX;fpP<(_dd2_@LohWIe(K98hjXB+!i5K$JYiU;XH;QOO{O2x?o>l64&c| zlx+9;;AH9ygnr&CV!v7i1DG;6vq3PP8Authj5+9B&*LgSfTk&E`exoMf14GY!=JW4f$cG3O05yoDJhzH)9cex2|cF7aY9GWauOL1!5%Hf%3|7Y z4>K8h=Br9csW6(LQW=6)h2f-DrDg|8kCBO3nx61l6i7!hTs9bE`7}N|emY8t(`rT@ z;M_Pv_KAa% z1qRHvVH$whH;dN~GdO^=BDdQ(QdCqtK6G{aBV}y;8G-Dc=KP)Q%j=@QOKcfxGJ(CS zR~l2z?g4jX-pN^SrnL@cw(V{9%)xeB+Vw7J<$WG)C%Uej-1Trn|2cQR_(>ts(;&T+ zJ&>pSc=BxV?_TSgD&6z$hz-TtO34q(+>5gekkn|ibzFAvZ(hM^yYTCc` T@B6B!|I~=>?ts;QQ(pf88iERQ 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 6a2de5fcf44582683330a22eeca5e4a51b8afbce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1449 zcmXqLVqI#`#L~NfnTe5!i6i7s#2nQNQPKvyY@Awc9&O)w85y}*84M~7bq%!Hm_u2Z zc~o3d%Tjal3sQ>|JY5u=Q;SM6(=wA2OEUBG6dX%SGV+TuODYZ33{*hMxp`zA3kq^l zajFwG5Co~?;^Bg*^UO=uGvqel1PQVUGld2l$cghB85)=vnHgFd8<`kI0l8*Ct~r=% z7-i5n*-*+rf{lxFl3M5FPdX`#j67Vv-bO~gQ2~|~=7!b=mJoLvp}5;Kuec;JCnvSY zO2NG-H9fzmQX#l1zbIKj1EE*b5b8Q^6cfb^L?BM)MbU}s>Lx}dywP*iB>BAE~mg$9-aw*8>aWgyb&Wn2X@R2g(r?QD1`|V0UL|;<8 zbT#63{QV(_&hJ;S z$-DLU^QTpLx(l-HlNWaSm9QO5Q=c1{^7Q(Tcvn;9^Oc-px{PL5Cq@_6?fD*9`2E_k z)~ye3w;YrzR6A_%EV9CaxnkNX)}&=TzJF&FtPd;U{kHz&=Cd2Pe^|Sg1RmcoMb!A( zs;teclx8oV8n;(L{7y}A(s?FkMh3>kO-v#NO-up?Jiq{z6=r1o&%$KDU?30TDYHlz zh&70Wi@#lT{!Yz|g#r)$>}%K6`Se&k#K3`#Lz|6}m6e^5k;TNo$iM)`H(+d2$tWo) zu+rDhPcAOdO9Z7oz2y8{FxS9XHzla zX>Mw{P6fpQcb+oT`IjQ9(XXFUT^WO1S-?%vVs9m|T(xbRN*mlGNPPypm!)12ecM7{#=p zK~r8{jubKoLsAoA*;n5{927jNpcvsb;D(0`kOK}kJ{B<+5tjp2&(=Bbev+-KcISkh zj@IFGMW`dFBm*c*Ux$ptP)l2h0i6By}xfvK~AP{9BjHG}MoC1Kw z4QlBF%sGq<#syoCMEtq2UO+CXY0?&(6{UTT)_wf;XWO}E=-bMe4?hFZ~eagA{(~Ea%9<@WS*9! zVRvR?WJJGC;*XlDrDuFXYdZd%_?ef-q{=P8b*tl8#D3=j;V1hai0LRu^v@J4{@&8? z_Qmyi>3@DiD6j?0$a}#2@BUO%(Hma7U1fiK-*;5}OVk1LMSQi!J@rQu3|~9!T(fKP YmC{c?cXkDxa9+D9iXr5j$-7ru0Z3^J3IG5A diff --git a/apple-codesign/src/testdata/ed25519.pk8 b/apple-codesign/src/testdata/ed25519.pk8 deleted file mode 100644 index b7ffa38037699f2d4cada6d4c9e146e7d4d62d7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48 zcmV-00MGw0E&>4nFa-t!D`jv5A_O2K#05BG08OZ7M#&`4JHU&LNQUrr!ay9qXGc{0)hbn0Iks+T>UO} zbjf4vC^AYvBwds{&|GcXYn__>&-=A5S0cNc3jHH!iUKJ^@hyD;mUuexkSDK3>~Rev4%pN@KA4XuUj z1OUKXih*u41A44&isnOh@aZFUmWe*G2e`EmjIn+|^~Lp?P?v*#&yyq4%IZq;mbjj` zDV4D;Me`F#Yz-KZcR1wpQ9A;~#OJq>ZQiqTHq@}Fm@?6e0-sV(zZ`>|+1@f1JiXYb zf1Nx!-puQtGKccC3rl>exHkXi*D~BTWk|JT=Y)|~q%OlRdjl9+S&HJ86EK}gV)u|R zz-g0PH!gQG>UgGwpAG{W_WzS?vOWfE3Tp9?WuxX0*RzkIJC%ZF-Im5SaHl zfDWPL2TDJ84vLRMlPCgSjmSa#B%~GoB%QN|6WEF3ZvNlgCwyfEj(E*j`+EPAS1(lF z^oCxup^U13I+inU0bEw-CcBQ|DF%}!G@X0q`nVD3h}C)6G8Y(szg;9E?%b5JE=PqM5L1tG>axSc#xeH`{kSc9 frbsT(aPx>xR+s8tCI%__hB-G12&9q^n@RC~!Ztm) diff --git a/apple-codesign/src/testdata/secp256r1.pk8 b/apple-codesign/src/testdata/secp256r1.pk8 deleted file mode 100644 index a6961708bfd841d597f7b6b2c16d19dc3a1e54cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmV;50CoQ`frkPC05B5<2P%e0&OHJF1_&yKNX|V20S5$aFlzz<0R$jf!E=z%#VLSq zjXtD(V{w_iZQU8YnoOtt%4CoTRyUEML<2$q1aDXW%DKf4A74u>RX2E6dp~KVVX&)< szrMRjA6F4Z8O)@CrWa4X`Ee)c3`Q9z{`6aY;k5AvP<)cN=CjXSLH24llmGw# 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