electron-cli 0.3.0-alpha.16 → 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 +2610 -88
- package/Cargo.toml +2 -1
- package/README.md +7 -4
- package/package.json +1 -1
- package/src/commands/package.rs +618 -3
package/src/commands/package.rs
CHANGED
|
@@ -5,6 +5,10 @@ use std::{
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
use anyhow::{bail, Context, Result};
|
|
8
|
+
use apple_codesign::{
|
|
9
|
+
cryptography::{parse_pfx_data, PrivateKey},
|
|
10
|
+
BundleSigner, CodeSignatureFlags, SettingsScope, SigningSettings,
|
|
11
|
+
};
|
|
8
12
|
use camino::Utf8PathBuf;
|
|
9
13
|
use plist::{Dictionary as PlistDictionary, Value as PlistValue};
|
|
10
14
|
use serde::Serialize;
|
|
@@ -71,7 +75,15 @@ struct MacosSigningPlan {
|
|
|
71
75
|
struct MacosSignPlan {
|
|
72
76
|
configured: bool,
|
|
73
77
|
enabled: bool,
|
|
78
|
+
will_execute: bool,
|
|
79
|
+
method: Option<String>,
|
|
74
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,
|
|
75
87
|
entitlements: Vec<Utf8PathBuf>,
|
|
76
88
|
entitlements_inherit: Option<Utf8PathBuf>,
|
|
77
89
|
hardened_runtime: Option<bool>,
|
|
@@ -87,6 +99,29 @@ struct MacosNotarizePlan {
|
|
|
87
99
|
keychain: Option<String>,
|
|
88
100
|
}
|
|
89
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
|
+
|
|
90
125
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
91
126
|
#[serde(rename_all = "kebab-case")]
|
|
92
127
|
enum PackageStatus {
|
|
@@ -124,6 +159,10 @@ struct MacosSignConfig {
|
|
|
124
159
|
enabled: bool,
|
|
125
160
|
invalid_type: bool,
|
|
126
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>,
|
|
127
166
|
entitlements: Vec<String>,
|
|
128
167
|
entitlements_inherit: Option<String>,
|
|
129
168
|
hardened_runtime: Option<bool>,
|
|
@@ -363,10 +402,163 @@ pub(crate) fn execute_package(report: &PackageReport, force: bool) -> Result<()>
|
|
|
363
402
|
&app_dir,
|
|
364
403
|
&report.project,
|
|
365
404
|
)?;
|
|
405
|
+
execute_macos_signing(report)?;
|
|
406
|
+
|
|
407
|
+
Ok(())
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
fn execute_macos_signing(report: &PackageReport) -> Result<()> {
|
|
411
|
+
if report.platform != "darwin" || !report.signing.macos.sign.will_execute {
|
|
412
|
+
return Ok(());
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
let bundle_dir = Path::new(report.bundle_dir.as_str());
|
|
416
|
+
let bundle_parent = bundle_dir
|
|
417
|
+
.parent()
|
|
418
|
+
.context("macOS bundle output has no parent directory")?;
|
|
419
|
+
let bundle_name = bundle_dir
|
|
420
|
+
.file_name()
|
|
421
|
+
.context("macOS bundle output has no bundle directory name")?;
|
|
422
|
+
let unique_suffix = std::time::SystemTime::now()
|
|
423
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
424
|
+
.context("system clock is before the Unix epoch")?
|
|
425
|
+
.as_nanos();
|
|
426
|
+
let signing_parent = bundle_parent.join(format!(
|
|
427
|
+
".electron-cli-signing-{}-{unique_suffix}",
|
|
428
|
+
std::process::id()
|
|
429
|
+
));
|
|
430
|
+
let signed_bundle_dir = signing_parent.join(bundle_name);
|
|
431
|
+
|
|
432
|
+
if signing_parent.exists() {
|
|
433
|
+
fs::remove_dir_all(&signing_parent)
|
|
434
|
+
.with_context(|| format!("Could not remove {}", signing_parent.display()))?;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let signing_result = (|| -> Result<()> {
|
|
438
|
+
let mut signer = BundleSigner::new_from_path(bundle_dir).with_context(|| {
|
|
439
|
+
format!(
|
|
440
|
+
"Could not prepare macOS bundle signing for {}",
|
|
441
|
+
bundle_dir.display()
|
|
442
|
+
)
|
|
443
|
+
})?;
|
|
444
|
+
signer
|
|
445
|
+
.collect_nested_bundles()
|
|
446
|
+
.context("Could not discover nested macOS bundles for signing")?;
|
|
447
|
+
|
|
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
|
+
}
|
|
478
|
+
|
|
479
|
+
Ok(())
|
|
480
|
+
})();
|
|
481
|
+
|
|
482
|
+
if let Err(error) = signing_result {
|
|
483
|
+
let _ = fs::remove_dir_all(&signing_parent);
|
|
484
|
+
return Err(error);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
fs::remove_dir_all(bundle_dir)
|
|
488
|
+
.with_context(|| format!("Could not remove {}", bundle_dir.display()))?;
|
|
489
|
+
fs::rename(&signed_bundle_dir, bundle_dir).with_context(|| {
|
|
490
|
+
format!(
|
|
491
|
+
"Could not move signed macOS bundle from {} to {}",
|
|
492
|
+
signed_bundle_dir.display(),
|
|
493
|
+
bundle_dir.display()
|
|
494
|
+
)
|
|
495
|
+
})?;
|
|
496
|
+
let _ = fs::remove_dir_all(&signing_parent);
|
|
366
497
|
|
|
367
498
|
Ok(())
|
|
368
499
|
}
|
|
369
500
|
|
|
501
|
+
fn macos_signing_settings<'key>(report: &PackageReport) -> Result<SigningSettings<'key>> {
|
|
502
|
+
let sign = &report.signing.macos.sign;
|
|
503
|
+
let mut settings = SigningSettings::default();
|
|
504
|
+
settings.set_binary_identifier(SettingsScope::Main, &report.metadata.bundle_identifier);
|
|
505
|
+
|
|
506
|
+
if sign.hardened_runtime.unwrap_or(false) {
|
|
507
|
+
settings.add_code_signature_flags(SettingsScope::Main, CodeSignatureFlags::RUNTIME);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if let Some(entitlements) = sign.entitlements.first() {
|
|
511
|
+
let entitlements_path = Path::new(entitlements.as_str());
|
|
512
|
+
let entitlements_xml = fs::read_to_string(entitlements_path).with_context(|| {
|
|
513
|
+
format!(
|
|
514
|
+
"Could not read macOS entitlements file {}",
|
|
515
|
+
entitlements_path.display()
|
|
516
|
+
)
|
|
517
|
+
})?;
|
|
518
|
+
settings
|
|
519
|
+
.set_entitlements_xml(SettingsScope::Main, entitlements_xml)
|
|
520
|
+
.with_context(|| {
|
|
521
|
+
format!(
|
|
522
|
+
"Could not parse macOS entitlements file {}",
|
|
523
|
+
entitlements_path.display()
|
|
524
|
+
)
|
|
525
|
+
})?;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
Ok(settings)
|
|
529
|
+
}
|
|
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
|
+
|
|
370
562
|
fn print_report(report: &PackageReport, json: bool) -> Result<()> {
|
|
371
563
|
if json {
|
|
372
564
|
return output::json(report);
|
|
@@ -403,6 +595,23 @@ fn print_report(report: &PackageReport, json: bool) -> Result<()> {
|
|
|
403
595
|
if let Some(identity) = &report.signing.macos.sign.identity {
|
|
404
596
|
println!(" identity: {identity}");
|
|
405
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
|
+
}
|
|
604
|
+
if let Some(method) = &report.signing.macos.sign.method {
|
|
605
|
+
println!(" signing method: {method}");
|
|
606
|
+
}
|
|
607
|
+
println!(
|
|
608
|
+
" signing execution: {}",
|
|
609
|
+
if report.signing.macos.sign.will_execute {
|
|
610
|
+
"enabled"
|
|
611
|
+
} else {
|
|
612
|
+
"not available"
|
|
613
|
+
}
|
|
614
|
+
);
|
|
406
615
|
println!(
|
|
407
616
|
" macOS notarization: {}",
|
|
408
617
|
if report.signing.macos.notarize.enabled {
|
|
@@ -533,6 +742,26 @@ fn parse_macos_sign_config(value: Option<&JsonValue>) -> MacosSignConfig {
|
|
|
533
742
|
.or_else(|| object.get("identityName"))
|
|
534
743
|
.and_then(JsonValue::as_str)
|
|
535
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),
|
|
536
765
|
entitlements,
|
|
537
766
|
entitlements_inherit: object
|
|
538
767
|
.get("entitlementsInherit")
|
|
@@ -720,21 +949,119 @@ fn macos_sign_plan(
|
|
|
720
949
|
.filter(|path| !path.trim().is_empty())
|
|
721
950
|
.map(|path| utf8_path(resolve_project_path(root, path)))
|
|
722
951
|
.transpose()?;
|
|
952
|
+
if let Some(path) = &entitlements_inherit {
|
|
953
|
+
if !Path::new(path.as_str()).exists() {
|
|
954
|
+
warnings.push(format!(
|
|
955
|
+
"Configured macOS inherited entitlements file does not exist: {}.",
|
|
956
|
+
path
|
|
957
|
+
));
|
|
958
|
+
}
|
|
959
|
+
}
|
|
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
|
+
|
|
1007
|
+
let identity = config.identity.as_deref().map(str::trim);
|
|
1008
|
+
let ad_hoc_identity = matches!(identity, None | Some("-"));
|
|
1009
|
+
let p12_identity = p12_file.is_some();
|
|
1010
|
+
let will_execute = config.enabled && platform == "darwin" && (ad_hoc_identity || p12_identity);
|
|
1011
|
+
let method = if config.enabled && platform == "darwin" {
|
|
1012
|
+
if p12_identity {
|
|
1013
|
+
Some("certificate-p12".to_string())
|
|
1014
|
+
} else if ad_hoc_identity {
|
|
1015
|
+
Some("ad-hoc".to_string())
|
|
1016
|
+
} else {
|
|
1017
|
+
Some("certificate-identity".to_string())
|
|
1018
|
+
}
|
|
1019
|
+
} else {
|
|
1020
|
+
None
|
|
1021
|
+
};
|
|
723
1022
|
|
|
724
1023
|
if config.configured && platform != "darwin" {
|
|
725
1024
|
warnings.push(format!(
|
|
726
1025
|
"macOS signing is configured but ignored for target platform {platform}."
|
|
727
1026
|
));
|
|
728
|
-
} else if config.enabled {
|
|
1027
|
+
} else if config.enabled && !will_execute {
|
|
729
1028
|
warnings.push(
|
|
730
|
-
"macOS signing is configured, but Rust-native signing is not implemented yet; package output will be unsigned.".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(),
|
|
731
1030
|
);
|
|
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
|
+
}
|
|
1037
|
+
if config.entitlements.len() > 1 {
|
|
1038
|
+
warnings.push(
|
|
1039
|
+
"Rust-native macOS signing applies the first macOS entitlements file only; inherited/login-helper entitlement scoping is not implemented yet.".to_string(),
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
if config.entitlements_inherit.is_some() {
|
|
1043
|
+
warnings.push(
|
|
1044
|
+
"packagerConfig.osxSign.entitlementsInherit is recognized but not applied to nested bundles by Rust-native signing yet.".to_string(),
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
if config.gatekeeper_assess.is_some() {
|
|
1048
|
+
warnings.push(
|
|
1049
|
+
"packagerConfig.osxSign.gatekeeperAssess is recognized but Gatekeeper assessment is not implemented yet.".to_string(),
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
732
1052
|
}
|
|
733
1053
|
|
|
734
1054
|
Ok(MacosSignPlan {
|
|
735
1055
|
configured: config.configured,
|
|
736
1056
|
enabled: config.enabled,
|
|
1057
|
+
will_execute,
|
|
1058
|
+
method,
|
|
737
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()),
|
|
738
1065
|
entitlements,
|
|
739
1066
|
entitlements_inherit,
|
|
740
1067
|
hardened_runtime: config.hardened_runtime,
|
|
@@ -769,6 +1096,24 @@ fn macos_notarize_plan(
|
|
|
769
1096
|
"macOS notarization requires packagerConfig.osxSign to be enabled first.".to_string(),
|
|
770
1097
|
);
|
|
771
1098
|
}
|
|
1099
|
+
if config.enabled
|
|
1100
|
+
&& platform == "darwin"
|
|
1101
|
+
&& package_config.packager.osx_sign.enabled
|
|
1102
|
+
&& package_config.packager.osx_sign.p12_file.is_none()
|
|
1103
|
+
&& matches!(
|
|
1104
|
+
package_config
|
|
1105
|
+
.packager
|
|
1106
|
+
.osx_sign
|
|
1107
|
+
.identity
|
|
1108
|
+
.as_deref()
|
|
1109
|
+
.map(str::trim),
|
|
1110
|
+
None | Some("-")
|
|
1111
|
+
)
|
|
1112
|
+
{
|
|
1113
|
+
warnings.push(
|
|
1114
|
+
"macOS notarization requires a Developer ID signature; Rust-native ad-hoc signing is not notarizable.".to_string(),
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
772
1117
|
if config.enabled && auth_method.is_none() {
|
|
773
1118
|
warnings.push(
|
|
774
1119
|
"macOS notarization config is missing a complete notarytool authentication set: appleId/appleIdPassword/teamId, appleApiKey/appleApiKeyId/appleApiIssuer, or keychainProfile.".to_string(),
|
|
@@ -1134,6 +1479,7 @@ fn apply_macos_metadata(report: &PackageReport) -> Result<()> {
|
|
|
1134
1479
|
"CFBundleIdentifier",
|
|
1135
1480
|
&report.metadata.bundle_identifier,
|
|
1136
1481
|
);
|
|
1482
|
+
set_plist_string(&mut dictionary, "CFBundlePackageType", "APPL");
|
|
1137
1483
|
|
|
1138
1484
|
if let Some(version) = &report.metadata.app_version {
|
|
1139
1485
|
set_plist_string(&mut dictionary, "CFBundleShortVersionString", version);
|
|
@@ -1816,6 +2162,11 @@ mod tests {
|
|
|
1816
2162
|
|
|
1817
2163
|
assert!(report.signing.macos.sign.configured);
|
|
1818
2164
|
assert!(report.signing.macos.sign.enabled);
|
|
2165
|
+
assert!(!report.signing.macos.sign.will_execute);
|
|
2166
|
+
assert_eq!(
|
|
2167
|
+
report.signing.macos.sign.method.as_deref(),
|
|
2168
|
+
Some("certificate-identity")
|
|
2169
|
+
);
|
|
1819
2170
|
assert_eq!(
|
|
1820
2171
|
report.signing.macos.sign.identity.as_deref(),
|
|
1821
2172
|
Some("Developer ID Application: Example, Inc. (TEAMID1234)")
|
|
@@ -1831,7 +2182,7 @@ mod tests {
|
|
|
1831
2182
|
assert!(report
|
|
1832
2183
|
.warnings
|
|
1833
2184
|
.iter()
|
|
1834
|
-
.any(|warning| warning.contains("Rust-native
|
|
2185
|
+
.any(|warning| warning.contains("Rust-native keychain identity signing")));
|
|
1835
2186
|
assert!(report
|
|
1836
2187
|
.warnings
|
|
1837
2188
|
.iter()
|
|
@@ -1845,6 +2196,115 @@ mod tests {
|
|
|
1845
2196
|
let _ = fs::remove_dir_all(root);
|
|
1846
2197
|
}
|
|
1847
2198
|
|
|
2199
|
+
#[test]
|
|
2200
|
+
fn plans_macos_ad_hoc_signing_execution() {
|
|
2201
|
+
let root = unique_temp_dir("macos-ad-hoc-signing-plan");
|
|
2202
|
+
write_package_json(&root);
|
|
2203
|
+
fs::write(
|
|
2204
|
+
root.join("forge.config.js"),
|
|
2205
|
+
r#"
|
|
2206
|
+
module.exports = {
|
|
2207
|
+
packagerConfig: {
|
|
2208
|
+
osxSign: {
|
|
2209
|
+
identity: '-',
|
|
2210
|
+
hardenedRuntime: true,
|
|
2211
|
+
},
|
|
2212
|
+
},
|
|
2213
|
+
};
|
|
2214
|
+
"#,
|
|
2215
|
+
)
|
|
2216
|
+
.expect("forge config should be written");
|
|
2217
|
+
write_app_file(&root);
|
|
2218
|
+
write_fake_electron_dist(&root);
|
|
2219
|
+
|
|
2220
|
+
let args = PackageArgs {
|
|
2221
|
+
cwd: root.clone(),
|
|
2222
|
+
out_dir: PathBuf::from("out"),
|
|
2223
|
+
name: None,
|
|
2224
|
+
platform: Some("darwin".to_string()),
|
|
2225
|
+
arch: Some("arm64".to_string()),
|
|
2226
|
+
force: false,
|
|
2227
|
+
dry_run: true,
|
|
2228
|
+
json: true,
|
|
2229
|
+
};
|
|
2230
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2231
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2232
|
+
|
|
2233
|
+
assert!(report.signing.macos.sign.configured);
|
|
2234
|
+
assert!(report.signing.macos.sign.enabled);
|
|
2235
|
+
assert!(report.signing.macos.sign.will_execute);
|
|
2236
|
+
assert_eq!(report.signing.macos.sign.method.as_deref(), Some("ad-hoc"));
|
|
2237
|
+
assert_eq!(report.signing.macos.sign.identity.as_deref(), Some("-"));
|
|
2238
|
+
assert_eq!(report.signing.macos.sign.hardened_runtime, Some(true));
|
|
2239
|
+
assert!(!report.warnings.iter().any(|warning| {
|
|
2240
|
+
warning.contains("Rust-native keychain identity signing")
|
|
2241
|
+
|| warning.contains("Rust-native signing is not implemented")
|
|
2242
|
+
}));
|
|
2243
|
+
|
|
2244
|
+
let _ = fs::remove_dir_all(root);
|
|
2245
|
+
}
|
|
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
|
+
|
|
1848
2308
|
#[test]
|
|
1849
2309
|
fn warns_when_macos_notarization_is_configured_without_signing() {
|
|
1850
2310
|
let root = unique_temp_dir("notarize-without-sign");
|
|
@@ -1947,6 +2407,10 @@ mod tests {
|
|
|
1947
2407
|
plist_string(dictionary, "CFBundleIdentifier"),
|
|
1948
2408
|
Some("com.example.starter")
|
|
1949
2409
|
);
|
|
2410
|
+
assert_eq!(
|
|
2411
|
+
plist_string(dictionary, "CFBundlePackageType"),
|
|
2412
|
+
Some("APPL")
|
|
2413
|
+
);
|
|
1950
2414
|
assert_eq!(
|
|
1951
2415
|
plist_string(dictionary, "CFBundleShortVersionString"),
|
|
1952
2416
|
Some("2.3.4")
|
|
@@ -1970,6 +2434,122 @@ mod tests {
|
|
|
1970
2434
|
let _ = fs::remove_dir_all(root);
|
|
1971
2435
|
}
|
|
1972
2436
|
|
|
2437
|
+
#[test]
|
|
2438
|
+
fn packages_macos_bundle_with_ad_hoc_signature() {
|
|
2439
|
+
if current_platform() != "darwin" {
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
let root = unique_temp_dir("macos-ad-hoc-signing-execute");
|
|
2444
|
+
fs::write(
|
|
2445
|
+
root.join("package.json"),
|
|
2446
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","devDependencies":{"electron":"30.0.0"},"electronCli":{"packagerConfig":{"appBundleId":"com.example.signed","osxSign":true}}}"#,
|
|
2447
|
+
)
|
|
2448
|
+
.expect("package.json should be written");
|
|
2449
|
+
write_app_file(&root);
|
|
2450
|
+
write_macho_electron_dist(&root);
|
|
2451
|
+
|
|
2452
|
+
let args = PackageArgs {
|
|
2453
|
+
cwd: root.clone(),
|
|
2454
|
+
out_dir: PathBuf::from("out"),
|
|
2455
|
+
name: None,
|
|
2456
|
+
platform: None,
|
|
2457
|
+
arch: None,
|
|
2458
|
+
force: false,
|
|
2459
|
+
dry_run: false,
|
|
2460
|
+
json: false,
|
|
2461
|
+
};
|
|
2462
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2463
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2464
|
+
|
|
2465
|
+
assert!(report.signing.macos.sign.will_execute);
|
|
2466
|
+
assert_eq!(report.signing.macos.sign.method.as_deref(), Some("ad-hoc"));
|
|
2467
|
+
assert!(report.warnings.is_empty());
|
|
2468
|
+
|
|
2469
|
+
execute_package(&report, false).expect("package should succeed");
|
|
2470
|
+
|
|
2471
|
+
let bundle_dir = Path::new(report.bundle_dir.as_str());
|
|
2472
|
+
assert!(bundle_dir
|
|
2473
|
+
.join("Contents/_CodeSignature/CodeResources")
|
|
2474
|
+
.exists());
|
|
2475
|
+
|
|
2476
|
+
let executable = bundle_dir
|
|
2477
|
+
.join("Contents/MacOS")
|
|
2478
|
+
.join(&report.executable_name);
|
|
2479
|
+
let executable_data = fs::read(executable).expect("signed executable should read");
|
|
2480
|
+
let macho = apple_codesign::MachFile::parse(&executable_data)
|
|
2481
|
+
.expect("signed executable should parse as Mach-O");
|
|
2482
|
+
assert!(macho.iter_macho().all(|binary| binary
|
|
2483
|
+
.code_signature()
|
|
2484
|
+
.expect("code signature should parse")
|
|
2485
|
+
.is_some()));
|
|
2486
|
+
|
|
2487
|
+
let _ = fs::remove_dir_all(root);
|
|
2488
|
+
}
|
|
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
|
+
|
|
1973
2553
|
#[test]
|
|
1974
2554
|
fn missing_required_runtime_dependency_fails() {
|
|
1975
2555
|
let root = unique_temp_dir("runtime-deps");
|
|
@@ -2123,6 +2703,41 @@ mod tests {
|
|
|
2123
2703
|
}
|
|
2124
2704
|
}
|
|
2125
2705
|
|
|
2706
|
+
fn write_macho_electron_dist(root: &Path) {
|
|
2707
|
+
let app = root.join("node_modules/electron/dist/Electron.app/Contents/MacOS");
|
|
2708
|
+
fs::create_dir_all(&app).expect("macOS Electron app should be created");
|
|
2709
|
+
fs::copy(
|
|
2710
|
+
std::env::current_exe().expect("current test executable should resolve"),
|
|
2711
|
+
app.join("Electron"),
|
|
2712
|
+
)
|
|
2713
|
+
.expect("Mach-O test executable should be copied");
|
|
2714
|
+
}
|
|
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
|
+
|
|
2126
2741
|
fn unique_temp_dir(label: &str) -> PathBuf {
|
|
2127
2742
|
let nanos = std::time::SystemTime::now()
|
|
2128
2743
|
.duration_since(std::time::UNIX_EPOCH)
|