electron-cli 0.3.0-alpha.17 → 0.3.0-alpha.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.lock CHANGED
@@ -953,7 +953,7 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
953
953
 
954
954
  [[package]]
955
955
  name = "electron-cli"
956
- version = "0.3.0-alpha.17"
956
+ version = "0.3.0-alpha.18"
957
957
  dependencies = [
958
958
  "anyhow",
959
959
  "apple-codesign",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "electron-cli"
3
- version = "0.3.0-alpha.17"
3
+ version = "0.3.0-alpha.18"
4
4
  edition = "2021"
5
5
  description = "Experimental Rust CLI for Electron project diagnostics and workflow automation"
6
6
  license = "MIT"
package/README.md CHANGED
@@ -41,9 +41,9 @@ The Rust-native flow currently owns:
41
41
 
42
42
  The GitHub publisher creates or reuses a release, uploads selected make artifacts, and can replace an existing asset with `--force`. It reads `GITHUB_TOKEN` or `GH_TOKEN` and can infer `OWNER/REPO` from package metadata, Forge GitHub publisher config, or `package.json` `repository`. You can also pass `--github-repo`.
43
43
 
44
- The package command recognizes macOS `packagerConfig.osxSign` and `packagerConfig.osxNotarize` options and reports the signing/notarization plan without serializing credential values. When `osxSign` is enabled on macOS and no certificate identity is configured, or the identity is `"-"`, `package` writes an experimental Rust-native ad-hoc signature for the generated `.app` bundle. Developer ID certificate/keychain signing and notarization execution are not implemented yet.
44
+ The package command recognizes macOS `packagerConfig.osxSign` and `packagerConfig.osxNotarize` options and reports the signing/notarization plan without serializing credential values. When `osxSign` is enabled on macOS and no certificate identity is configured, or the identity is `"-"`, `package` writes an experimental Rust-native ad-hoc signature for the generated `.app` bundle. When `osxSign.p12File` points at a `.p12`/PFX certificate export, `package` can sign the bundle with that certificate. macOS keychain identity lookup and notarization execution are not implemented yet.
45
45
 
46
- The DMG maker is currently a pure-Rust FAT32 image with the app bundle and an Applications entry. The MSI maker writes a compressed embedded CAB, Windows Installer database tables, and a Start Menu shortcut when the packaged executable is present. HFS+/APFS DMG layout customization, installer UI customization, Windows/Linux icon embedding, Developer ID signing execution, and notarization execution are still TODO.
46
+ The DMG maker is currently a pure-Rust FAT32 image with the app bundle and an Applications entry. The MSI maker writes a compressed embedded CAB, Windows Installer database tables, and a Start Menu shortcut when the packaged executable is present. HFS+/APFS DMG layout customization, installer UI customization, Windows/Linux icon embedding, macOS keychain signing, timestamping, and notarization execution are still TODO.
47
47
 
48
48
  Package metadata can be configured in `package.json`:
49
49
 
@@ -57,7 +57,8 @@ Package metadata can be configured in `package.json`:
57
57
  "icon": "assets/icon",
58
58
  "extraResource": "assets/config.json",
59
59
  "osxSign": {
60
- "identity": "-",
60
+ "p12File": "certs/developer-id.p12",
61
+ "p12PasswordEnv": "ELECTRON_CLI_P12_PASSWORD",
61
62
  "entitlements": "assets/entitlements.plist",
62
63
  "hardenedRuntime": true
63
64
  },
@@ -97,7 +98,7 @@ Package metadata can be configured in `package.json`:
97
98
  }
98
99
  ```
99
100
 
100
- Set `identity` to a Developer ID certificate name when you want the plan to reflect Forge-style release signing, but this project will report it as not executable until Rust-native certificate/keychain signing exists. Use `identity: "-"` or omit `identity` for the current ad-hoc signing path.
101
+ Use `p12PasswordEnv`, `p12PasswordFile`, or `p12Password` for the `.p12` password; password values are not serialized in package reports. Set `identity` to a Developer ID certificate name when you want the plan to reflect Forge-style keychain release signing, but this project will report it as not executable until Rust-native keychain lookup exists. Use `identity: "-"` or omit `identity` for the current ad-hoc signing path.
101
102
 
102
103
  The package command also reads JSON-shaped `config.forge.packagerConfig` and `electronPackagerConfig` entries for the same fields. The make command maps JSON-shaped Forge maker names to the Rust-native targets it supports: zip, dmg, deb, rpm, and wix/msi. The publish command maps JSON-shaped publisher names to local and GitHub.
103
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electron-cli",
3
- "version": "0.3.0-alpha.17",
3
+ "version": "0.3.0-alpha.18",
4
4
  "description": "Experimental Rust CLI for Electron project diagnostics and workflow automation",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -5,7 +5,10 @@ use std::{
5
5
  };
6
6
 
7
7
  use anyhow::{bail, Context, Result};
8
- use apple_codesign::{BundleSigner, CodeSignatureFlags, SettingsScope, SigningSettings};
8
+ use apple_codesign::{
9
+ cryptography::{parse_pfx_data, PrivateKey},
10
+ BundleSigner, CodeSignatureFlags, SettingsScope, SigningSettings,
11
+ };
9
12
  use camino::Utf8PathBuf;
10
13
  use plist::{Dictionary as PlistDictionary, Value as PlistValue};
11
14
  use serde::Serialize;
@@ -75,6 +78,12 @@ struct MacosSignPlan {
75
78
  will_execute: bool,
76
79
  method: Option<String>,
77
80
  identity: Option<String>,
81
+ p12_file: Option<Utf8PathBuf>,
82
+ p12_password_source: Option<String>,
83
+ p12_password_env: Option<String>,
84
+ p12_password_file: Option<Utf8PathBuf>,
85
+ #[serde(skip)]
86
+ p12_password: RedactedSecret,
78
87
  entitlements: Vec<Utf8PathBuf>,
79
88
  entitlements_inherit: Option<Utf8PathBuf>,
80
89
  hardened_runtime: Option<bool>,
@@ -90,6 +99,29 @@ struct MacosNotarizePlan {
90
99
  keychain: Option<String>,
91
100
  }
92
101
 
102
+ #[derive(Clone, Default)]
103
+ struct RedactedSecret(Option<String>);
104
+
105
+ impl std::fmt::Debug for RedactedSecret {
106
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107
+ if self.0.is_some() {
108
+ formatter.write_str("<redacted>")
109
+ } else {
110
+ formatter.write_str("<unset>")
111
+ }
112
+ }
113
+ }
114
+
115
+ impl RedactedSecret {
116
+ fn new(value: Option<String>) -> Self {
117
+ Self(value)
118
+ }
119
+
120
+ fn as_deref(&self) -> Option<&str> {
121
+ self.0.as_deref()
122
+ }
123
+ }
124
+
93
125
  #[derive(Clone, Copy, Debug, Serialize)]
94
126
  #[serde(rename_all = "kebab-case")]
95
127
  enum PackageStatus {
@@ -127,6 +159,10 @@ struct MacosSignConfig {
127
159
  enabled: bool,
128
160
  invalid_type: bool,
129
161
  identity: Option<String>,
162
+ p12_file: Option<String>,
163
+ p12_password: Option<String>,
164
+ p12_password_env: Option<String>,
165
+ p12_password_file: Option<String>,
130
166
  entitlements: Vec<String>,
131
167
  entitlements_inherit: Option<String>,
132
168
  hardened_runtime: Option<bool>,
@@ -409,15 +445,36 @@ fn execute_macos_signing(report: &PackageReport) -> Result<()> {
409
445
  .collect_nested_bundles()
410
446
  .context("Could not discover nested macOS bundles for signing")?;
411
447
 
412
- let settings = macos_signing_settings(report)?;
413
- signer
414
- .write_signed_bundle(&signed_bundle_dir, &settings)
415
- .with_context(|| {
416
- format!(
417
- "Could not write signed macOS bundle to {}",
418
- signed_bundle_dir.display()
419
- )
420
- })?;
448
+ let mut settings = macos_signing_settings(report)?;
449
+ if let Some(p12_file) = &report.signing.macos.sign.p12_file {
450
+ let p12_path = Path::new(p12_file.as_str());
451
+ let p12_data = fs::read(p12_path)
452
+ .with_context(|| format!("Could not read {}", p12_path.display()))?;
453
+ let password = macos_p12_password(&report.signing.macos.sign)?;
454
+ let (certificate, signing_key) = parse_pfx_data(&p12_data, &password)
455
+ .with_context(|| format!("Could not parse {}", p12_path.display()))?;
456
+
457
+ settings.set_signing_key(signing_key.as_key_info_signer(), certificate);
458
+ settings.chain_apple_certificates();
459
+ settings.set_team_id_from_signing_certificate();
460
+ signer
461
+ .write_signed_bundle(&signed_bundle_dir, &settings)
462
+ .with_context(|| {
463
+ format!(
464
+ "Could not write signed macOS bundle to {}",
465
+ signed_bundle_dir.display()
466
+ )
467
+ })?;
468
+ } else {
469
+ signer
470
+ .write_signed_bundle(&signed_bundle_dir, &settings)
471
+ .with_context(|| {
472
+ format!(
473
+ "Could not write signed macOS bundle to {}",
474
+ signed_bundle_dir.display()
475
+ )
476
+ })?;
477
+ }
421
478
 
422
479
  Ok(())
423
480
  })();
@@ -441,7 +498,7 @@ fn execute_macos_signing(report: &PackageReport) -> Result<()> {
441
498
  Ok(())
442
499
  }
443
500
 
444
- fn macos_signing_settings(report: &PackageReport) -> Result<SigningSettings<'static>> {
501
+ fn macos_signing_settings<'key>(report: &PackageReport) -> Result<SigningSettings<'key>> {
445
502
  let sign = &report.signing.macos.sign;
446
503
  let mut settings = SigningSettings::default();
447
504
  settings.set_binary_identifier(SettingsScope::Main, &report.metadata.bundle_identifier);
@@ -471,6 +528,37 @@ fn macos_signing_settings(report: &PackageReport) -> Result<SigningSettings<'sta
471
528
  Ok(settings)
472
529
  }
473
530
 
531
+ fn macos_p12_password(sign: &MacosSignPlan) -> Result<String> {
532
+ if let Some(password) = sign.p12_password.as_deref() {
533
+ return Ok(password.to_string());
534
+ }
535
+
536
+ if let Some(env_name) = &sign.p12_password_env {
537
+ return std::env::var(env_name)
538
+ .with_context(|| format!("Could not read macOS signing p12 password env {env_name}"));
539
+ }
540
+
541
+ if let Some(path) = &sign.p12_password_file {
542
+ let password_path = Path::new(path.as_str());
543
+ return fs::read_to_string(password_path)
544
+ .with_context(|| {
545
+ format!(
546
+ "Could not read macOS signing p12 password file {}",
547
+ password_path.display()
548
+ )
549
+ })
550
+ .and_then(|contents| {
551
+ contents
552
+ .lines()
553
+ .next()
554
+ .map(str::to_string)
555
+ .context("macOS signing p12 password file is empty")
556
+ });
557
+ }
558
+
559
+ Ok(String::new())
560
+ }
561
+
474
562
  fn print_report(report: &PackageReport, json: bool) -> Result<()> {
475
563
  if json {
476
564
  return output::json(report);
@@ -507,6 +595,12 @@ fn print_report(report: &PackageReport, json: bool) -> Result<()> {
507
595
  if let Some(identity) = &report.signing.macos.sign.identity {
508
596
  println!(" identity: {identity}");
509
597
  }
598
+ if let Some(path) = &report.signing.macos.sign.p12_file {
599
+ println!(" p12 file: {path}");
600
+ }
601
+ if let Some(source) = &report.signing.macos.sign.p12_password_source {
602
+ println!(" p12 password: {source}");
603
+ }
510
604
  if let Some(method) = &report.signing.macos.sign.method {
511
605
  println!(" signing method: {method}");
512
606
  }
@@ -648,6 +742,26 @@ fn parse_macos_sign_config(value: Option<&JsonValue>) -> MacosSignConfig {
648
742
  .or_else(|| object.get("identityName"))
649
743
  .and_then(JsonValue::as_str)
650
744
  .map(ToOwned::to_owned),
745
+ p12_file: object
746
+ .get("p12File")
747
+ .or_else(|| object.get("pfxFile"))
748
+ .and_then(JsonValue::as_str)
749
+ .map(ToOwned::to_owned),
750
+ p12_password: object
751
+ .get("p12Password")
752
+ .or_else(|| object.get("pfxPassword"))
753
+ .and_then(JsonValue::as_str)
754
+ .map(ToOwned::to_owned),
755
+ p12_password_env: object
756
+ .get("p12PasswordEnv")
757
+ .or_else(|| object.get("pfxPasswordEnv"))
758
+ .and_then(JsonValue::as_str)
759
+ .map(ToOwned::to_owned),
760
+ p12_password_file: object
761
+ .get("p12PasswordFile")
762
+ .or_else(|| object.get("pfxPasswordFile"))
763
+ .and_then(JsonValue::as_str)
764
+ .map(ToOwned::to_owned),
651
765
  entitlements,
652
766
  entitlements_inherit: object
653
767
  .get("entitlementsInherit")
@@ -844,11 +958,60 @@ fn macos_sign_plan(
844
958
  }
845
959
  }
846
960
 
961
+ let p12_file = config
962
+ .p12_file
963
+ .as_deref()
964
+ .filter(|path| !path.trim().is_empty())
965
+ .map(|path| utf8_path(resolve_project_path(root, path)))
966
+ .transpose()?;
967
+ if let Some(path) = &p12_file {
968
+ if !Path::new(path.as_str()).exists() {
969
+ warnings.push(format!(
970
+ "Configured macOS signing p12 file does not exist: {}.",
971
+ path
972
+ ));
973
+ }
974
+ }
975
+ let p12_password_file = config
976
+ .p12_password_file
977
+ .as_deref()
978
+ .filter(|path| !path.trim().is_empty())
979
+ .map(|path| utf8_path(resolve_project_path(root, path)))
980
+ .transpose()?;
981
+ if let Some(path) = &p12_password_file {
982
+ if !Path::new(path.as_str()).exists() {
983
+ warnings.push(format!(
984
+ "Configured macOS signing p12 password file does not exist: {}.",
985
+ path
986
+ ));
987
+ }
988
+ }
989
+ let p12_password_source = if p12_file.is_some() {
990
+ if config.p12_password.is_some() {
991
+ Some("config".to_string())
992
+ } else if let Some(env_name) = config
993
+ .p12_password_env
994
+ .as_deref()
995
+ .filter(|name| !name.trim().is_empty())
996
+ {
997
+ Some(format!("env:{env_name}"))
998
+ } else if let Some(path) = &p12_password_file {
999
+ Some(format!("file:{path}"))
1000
+ } else {
1001
+ Some("empty".to_string())
1002
+ }
1003
+ } else {
1004
+ None
1005
+ };
1006
+
847
1007
  let identity = config.identity.as_deref().map(str::trim);
848
1008
  let ad_hoc_identity = matches!(identity, None | Some("-"));
849
- let will_execute = config.enabled && platform == "darwin" && ad_hoc_identity;
1009
+ let p12_identity = p12_file.is_some();
1010
+ let will_execute = config.enabled && platform == "darwin" && (ad_hoc_identity || p12_identity);
850
1011
  let method = if config.enabled && platform == "darwin" {
851
- if ad_hoc_identity {
1012
+ if p12_identity {
1013
+ Some("certificate-p12".to_string())
1014
+ } else if ad_hoc_identity {
852
1015
  Some("ad-hoc".to_string())
853
1016
  } else {
854
1017
  Some("certificate-identity".to_string())
@@ -863,17 +1026,22 @@ fn macos_sign_plan(
863
1026
  ));
864
1027
  } else if config.enabled && !will_execute {
865
1028
  warnings.push(
866
- "macOS signing identity is configured, but Rust-native certificate/keychain signing is not implemented yet; package output will be unsigned. Use identity '-' or omit identity for experimental ad-hoc signing.".to_string(),
1029
+ "macOS signing identity is configured, but Rust-native keychain identity signing is not implemented yet; package output will be unsigned. Use p12File for certificate signing, or identity '-' / omit identity for experimental ad-hoc signing.".to_string(),
867
1030
  );
868
1031
  } else if will_execute {
1032
+ if p12_identity && identity.is_some() {
1033
+ warnings.push(
1034
+ "packagerConfig.osxSign.p12File supplies the signing certificate; identity is reported but not used for keychain lookup.".to_string(),
1035
+ );
1036
+ }
869
1037
  if config.entitlements.len() > 1 {
870
1038
  warnings.push(
871
- "Rust-native ad-hoc signing applies the first macOS entitlements file only; inherited/login-helper entitlement scoping is not implemented yet.".to_string(),
1039
+ "Rust-native macOS signing applies the first macOS entitlements file only; inherited/login-helper entitlement scoping is not implemented yet.".to_string(),
872
1040
  );
873
1041
  }
874
1042
  if config.entitlements_inherit.is_some() {
875
1043
  warnings.push(
876
- "packagerConfig.osxSign.entitlementsInherit is recognized but not applied to nested bundles by Rust-native ad-hoc signing yet.".to_string(),
1044
+ "packagerConfig.osxSign.entitlementsInherit is recognized but not applied to nested bundles by Rust-native signing yet.".to_string(),
877
1045
  );
878
1046
  }
879
1047
  if config.gatekeeper_assess.is_some() {
@@ -889,6 +1057,11 @@ fn macos_sign_plan(
889
1057
  will_execute,
890
1058
  method,
891
1059
  identity: config.identity.clone(),
1060
+ p12_file,
1061
+ p12_password_source,
1062
+ p12_password_env: config.p12_password_env.clone(),
1063
+ p12_password_file,
1064
+ p12_password: RedactedSecret::new(config.p12_password.clone()),
892
1065
  entitlements,
893
1066
  entitlements_inherit,
894
1067
  hardened_runtime: config.hardened_runtime,
@@ -926,6 +1099,7 @@ fn macos_notarize_plan(
926
1099
  if config.enabled
927
1100
  && platform == "darwin"
928
1101
  && package_config.packager.osx_sign.enabled
1102
+ && package_config.packager.osx_sign.p12_file.is_none()
929
1103
  && matches!(
930
1104
  package_config
931
1105
  .packager
@@ -2008,7 +2182,7 @@ mod tests {
2008
2182
  assert!(report
2009
2183
  .warnings
2010
2184
  .iter()
2011
- .any(|warning| warning.contains("Rust-native certificate/keychain signing")));
2185
+ .any(|warning| warning.contains("Rust-native keychain identity signing")));
2012
2186
  assert!(report
2013
2187
  .warnings
2014
2188
  .iter()
@@ -2063,13 +2237,74 @@ mod tests {
2063
2237
  assert_eq!(report.signing.macos.sign.identity.as_deref(), Some("-"));
2064
2238
  assert_eq!(report.signing.macos.sign.hardened_runtime, Some(true));
2065
2239
  assert!(!report.warnings.iter().any(|warning| {
2066
- warning.contains("Rust-native certificate/keychain signing")
2240
+ warning.contains("Rust-native keychain identity signing")
2067
2241
  || warning.contains("Rust-native signing is not implemented")
2068
2242
  }));
2069
2243
 
2070
2244
  let _ = fs::remove_dir_all(root);
2071
2245
  }
2072
2246
 
2247
+ #[test]
2248
+ fn plans_macos_p12_signing_without_serializing_password() {
2249
+ let root = unique_temp_dir("macos-p12-signing-plan");
2250
+ write_package_json(&root);
2251
+ fs::write(root.join("developer-id.p12"), "not a real p12")
2252
+ .expect("p12 placeholder should be written");
2253
+ fs::write(
2254
+ root.join("forge.config.js"),
2255
+ r#"
2256
+ module.exports = {
2257
+ packagerConfig: {
2258
+ osxSign: {
2259
+ identity: 'Developer ID Application: Example, Inc. (TEAMID1234)',
2260
+ p12File: 'developer-id.p12',
2261
+ p12Password: 'p12-secret',
2262
+ hardenedRuntime: true,
2263
+ },
2264
+ },
2265
+ };
2266
+ "#,
2267
+ )
2268
+ .expect("forge config should be written");
2269
+ write_app_file(&root);
2270
+ write_fake_electron_dist(&root);
2271
+
2272
+ let args = PackageArgs {
2273
+ cwd: root.clone(),
2274
+ out_dir: PathBuf::from("out"),
2275
+ name: None,
2276
+ platform: Some("darwin".to_string()),
2277
+ arch: Some("arm64".to_string()),
2278
+ force: false,
2279
+ dry_run: true,
2280
+ json: true,
2281
+ };
2282
+ let snapshot = crate::project::inspect(&root).expect("project should inspect");
2283
+ let report = build_report(snapshot, &args).expect("report should build");
2284
+
2285
+ assert!(report.signing.macos.sign.configured);
2286
+ assert!(report.signing.macos.sign.enabled);
2287
+ assert!(report.signing.macos.sign.will_execute);
2288
+ assert_eq!(
2289
+ report.signing.macos.sign.method.as_deref(),
2290
+ Some("certificate-p12")
2291
+ );
2292
+ assert_eq!(
2293
+ report.signing.macos.sign.p12_password_source.as_deref(),
2294
+ Some("config")
2295
+ );
2296
+ assert!(report.signing.macos.sign.p12_file.is_some());
2297
+ assert!(report
2298
+ .warnings
2299
+ .iter()
2300
+ .any(|warning| { warning.contains("p12File supplies the signing certificate") }));
2301
+
2302
+ let json = serde_json::to_string(&report).expect("report should serialize");
2303
+ assert!(!json.contains("p12-secret"));
2304
+
2305
+ let _ = fs::remove_dir_all(root);
2306
+ }
2307
+
2073
2308
  #[test]
2074
2309
  fn warns_when_macos_notarization_is_configured_without_signing() {
2075
2310
  let root = unique_temp_dir("notarize-without-sign");
@@ -2252,6 +2487,69 @@ mod tests {
2252
2487
  let _ = fs::remove_dir_all(root);
2253
2488
  }
2254
2489
 
2490
+ #[test]
2491
+ fn packages_macos_bundle_with_p12_certificate_signature() {
2492
+ if current_platform() != "darwin" {
2493
+ return;
2494
+ }
2495
+
2496
+ let Some(p12_fixture) = apple_codesign_test_fixture("apple-codesign-testuser.p12") else {
2497
+ return;
2498
+ };
2499
+
2500
+ let root = unique_temp_dir("macos-p12-signing-execute");
2501
+ fs::copy(&p12_fixture, root.join("developer-id.p12"))
2502
+ .expect("p12 fixture should be copied");
2503
+ fs::write(
2504
+ root.join("package.json"),
2505
+ r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","devDependencies":{"electron":"30.0.0"},"electronCli":{"packagerConfig":{"appBundleId":"com.example.p12-signed","osxSign":{"p12File":"developer-id.p12","p12Password":"password123","hardenedRuntime":true}}}}"#,
2506
+ )
2507
+ .expect("package.json should be written");
2508
+ write_app_file(&root);
2509
+ write_macho_electron_dist(&root);
2510
+
2511
+ let args = PackageArgs {
2512
+ cwd: root.clone(),
2513
+ out_dir: PathBuf::from("out"),
2514
+ name: None,
2515
+ platform: None,
2516
+ arch: None,
2517
+ force: false,
2518
+ dry_run: false,
2519
+ json: false,
2520
+ };
2521
+ let snapshot = crate::project::inspect(&root).expect("project should inspect");
2522
+ let report = build_report(snapshot, &args).expect("report should build");
2523
+
2524
+ assert!(report.signing.macos.sign.will_execute);
2525
+ assert_eq!(
2526
+ report.signing.macos.sign.method.as_deref(),
2527
+ Some("certificate-p12")
2528
+ );
2529
+ assert!(report.warnings.is_empty());
2530
+
2531
+ execute_package(&report, false).expect("package should succeed");
2532
+
2533
+ let executable = Path::new(report.bundle_dir.as_str())
2534
+ .join("Contents/MacOS")
2535
+ .join(&report.executable_name);
2536
+ let executable_data = fs::read(executable).expect("signed executable should read");
2537
+ let macho = apple_codesign::MachFile::parse(&executable_data)
2538
+ .expect("signed executable should parse as Mach-O");
2539
+ assert!(macho.iter_macho().all(|binary| {
2540
+ let signature = binary
2541
+ .code_signature()
2542
+ .expect("code signature should parse")
2543
+ .expect("code signature should exist");
2544
+ signature
2545
+ .signature_data()
2546
+ .expect("CMS signature should parse")
2547
+ .is_some_and(|data| !data.is_empty())
2548
+ }));
2549
+
2550
+ let _ = fs::remove_dir_all(root);
2551
+ }
2552
+
2255
2553
  #[test]
2256
2554
  fn missing_required_runtime_dependency_fails() {
2257
2555
  let root = unique_temp_dir("runtime-deps");
@@ -2415,6 +2713,31 @@ mod tests {
2415
2713
  .expect("Mach-O test executable should be copied");
2416
2714
  }
2417
2715
 
2716
+ fn apple_codesign_test_fixture(file_name: &str) -> Option<PathBuf> {
2717
+ let cargo_home = std::env::var_os("CARGO_HOME")
2718
+ .map(PathBuf::from)
2719
+ .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".cargo")))?;
2720
+ let registry_src = cargo_home.join("registry/src");
2721
+ for index_dir in fs::read_dir(registry_src).ok()? {
2722
+ let index_dir = index_dir.ok()?;
2723
+ for crate_dir in fs::read_dir(index_dir.path()).ok()? {
2724
+ let crate_dir = crate_dir.ok()?;
2725
+ let file_name_matches = crate_dir
2726
+ .file_name()
2727
+ .to_str()
2728
+ .is_some_and(|name| name.starts_with("apple-codesign-"));
2729
+ if file_name_matches {
2730
+ let candidate = crate_dir.path().join("src").join(file_name);
2731
+ if candidate.exists() {
2732
+ return Some(candidate);
2733
+ }
2734
+ }
2735
+ }
2736
+ }
2737
+
2738
+ None
2739
+ }
2740
+
2418
2741
  fn unique_temp_dir(label: &str) -> PathBuf {
2419
2742
  let nanos = std::time::SystemTime::now()
2420
2743
  .duration_since(std::time::UNIX_EPOCH)