electron-cli 0.3.0-alpha.17 → 0.3.0-alpha.19
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 +1 -1
- package/Cargo.toml +1 -1
- package/README.md +6 -4
- package/package.json +1 -1
- package/src/commands/package.rs +556 -19
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
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.
|
|
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 and can request a CMS timestamp token for notarization-compatible signatures. macOS keychain identity lookup and notarization submission/stapling 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,
|
|
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, and notarization execution are still TODO.
|
|
47
47
|
|
|
48
48
|
Package metadata can be configured in `package.json`:
|
|
49
49
|
|
|
@@ -57,7 +57,9 @@ Package metadata can be configured in `package.json`:
|
|
|
57
57
|
"icon": "assets/icon",
|
|
58
58
|
"extraResource": "assets/config.json",
|
|
59
59
|
"osxSign": {
|
|
60
|
-
"
|
|
60
|
+
"p12File": "certs/developer-id.p12",
|
|
61
|
+
"p12PasswordEnv": "ELECTRON_CLI_P12_PASSWORD",
|
|
62
|
+
"timestamp": true,
|
|
61
63
|
"entitlements": "assets/entitlements.plist",
|
|
62
64
|
"hardenedRuntime": true
|
|
63
65
|
},
|
|
@@ -97,7 +99,7 @@ Package metadata can be configured in `package.json`:
|
|
|
97
99
|
}
|
|
98
100
|
```
|
|
99
101
|
|
|
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
|
|
102
|
+
Use `p12PasswordEnv`, `p12PasswordFile`, or `p12Password` for the `.p12` password; password values are not serialized in package reports. Set `osxSign.timestamp` to a timestamp server URL, `true` for Apple's default `http://timestamp.apple.com/ts01`, or `"none"` / `false` to disable timestamping. When `osxNotarize` is enabled with p12 signing, `electron-cli` automatically enables notarization-compatible signing and uses Apple's timestamp server unless timestamping is disabled explicitly. 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
103
|
|
|
102
104
|
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
105
|
|
package/package.json
CHANGED
package/src/commands/package.rs
CHANGED
|
@@ -5,7 +5,10 @@ use std::{
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
use anyhow::{bail, Context, Result};
|
|
8
|
-
use apple_codesign::{
|
|
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;
|
|
@@ -13,6 +16,8 @@ use serde_json::Value as JsonValue;
|
|
|
13
16
|
|
|
14
17
|
use crate::{cli::PackageArgs, output, project::ProjectSnapshot};
|
|
15
18
|
|
|
19
|
+
const APPLE_TIMESTAMP_URL: &str = "http://timestamp.apple.com/ts01";
|
|
20
|
+
|
|
16
21
|
#[derive(Clone, Debug, Serialize)]
|
|
17
22
|
pub(crate) struct PackageReport {
|
|
18
23
|
project: ProjectSnapshot,
|
|
@@ -75,6 +80,14 @@ struct MacosSignPlan {
|
|
|
75
80
|
will_execute: bool,
|
|
76
81
|
method: Option<String>,
|
|
77
82
|
identity: Option<String>,
|
|
83
|
+
p12_file: Option<Utf8PathBuf>,
|
|
84
|
+
p12_password_source: Option<String>,
|
|
85
|
+
p12_password_env: Option<String>,
|
|
86
|
+
p12_password_file: Option<Utf8PathBuf>,
|
|
87
|
+
#[serde(skip)]
|
|
88
|
+
p12_password: RedactedSecret,
|
|
89
|
+
timestamp_url: Option<String>,
|
|
90
|
+
for_notarization: bool,
|
|
78
91
|
entitlements: Vec<Utf8PathBuf>,
|
|
79
92
|
entitlements_inherit: Option<Utf8PathBuf>,
|
|
80
93
|
hardened_runtime: Option<bool>,
|
|
@@ -90,6 +103,29 @@ struct MacosNotarizePlan {
|
|
|
90
103
|
keychain: Option<String>,
|
|
91
104
|
}
|
|
92
105
|
|
|
106
|
+
#[derive(Clone, Default)]
|
|
107
|
+
struct RedactedSecret(Option<String>);
|
|
108
|
+
|
|
109
|
+
impl std::fmt::Debug for RedactedSecret {
|
|
110
|
+
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
111
|
+
if self.0.is_some() {
|
|
112
|
+
formatter.write_str("<redacted>")
|
|
113
|
+
} else {
|
|
114
|
+
formatter.write_str("<unset>")
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
impl RedactedSecret {
|
|
120
|
+
fn new(value: Option<String>) -> Self {
|
|
121
|
+
Self(value)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn as_deref(&self) -> Option<&str> {
|
|
125
|
+
self.0.as_deref()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
93
129
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
94
130
|
#[serde(rename_all = "kebab-case")]
|
|
95
131
|
enum PackageStatus {
|
|
@@ -127,12 +163,24 @@ struct MacosSignConfig {
|
|
|
127
163
|
enabled: bool,
|
|
128
164
|
invalid_type: bool,
|
|
129
165
|
identity: Option<String>,
|
|
166
|
+
p12_file: Option<String>,
|
|
167
|
+
p12_password: Option<String>,
|
|
168
|
+
p12_password_env: Option<String>,
|
|
169
|
+
p12_password_file: Option<String>,
|
|
170
|
+
timestamp: Option<MacosTimestampConfig>,
|
|
130
171
|
entitlements: Vec<String>,
|
|
131
172
|
entitlements_inherit: Option<String>,
|
|
132
173
|
hardened_runtime: Option<bool>,
|
|
133
174
|
gatekeeper_assess: Option<bool>,
|
|
134
175
|
}
|
|
135
176
|
|
|
177
|
+
#[derive(Clone, Debug)]
|
|
178
|
+
enum MacosTimestampConfig {
|
|
179
|
+
Default,
|
|
180
|
+
Disabled,
|
|
181
|
+
Url(String),
|
|
182
|
+
}
|
|
183
|
+
|
|
136
184
|
#[derive(Clone, Debug, Default)]
|
|
137
185
|
struct MacosNotarizeConfig {
|
|
138
186
|
configured: bool,
|
|
@@ -409,15 +457,39 @@ fn execute_macos_signing(report: &PackageReport) -> Result<()> {
|
|
|
409
457
|
.collect_nested_bundles()
|
|
410
458
|
.context("Could not discover nested macOS bundles for signing")?;
|
|
411
459
|
|
|
412
|
-
let settings = macos_signing_settings(report)?;
|
|
413
|
-
|
|
414
|
-
.
|
|
415
|
-
|
|
416
|
-
format!(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
)
|
|
420
|
-
|
|
460
|
+
let mut settings = macos_signing_settings(report)?;
|
|
461
|
+
if let Some(p12_file) = &report.signing.macos.sign.p12_file {
|
|
462
|
+
let p12_path = Path::new(p12_file.as_str());
|
|
463
|
+
let p12_data = fs::read(p12_path)
|
|
464
|
+
.with_context(|| format!("Could not read {}", p12_path.display()))?;
|
|
465
|
+
let password = macos_p12_password(&report.signing.macos.sign)?;
|
|
466
|
+
let (certificate, signing_key) = parse_pfx_data(&p12_data, &password)
|
|
467
|
+
.with_context(|| format!("Could not parse {}", p12_path.display()))?;
|
|
468
|
+
|
|
469
|
+
settings.set_signing_key(signing_key.as_key_info_signer(), certificate);
|
|
470
|
+
settings.chain_apple_certificates();
|
|
471
|
+
settings.set_team_id_from_signing_certificate();
|
|
472
|
+
settings
|
|
473
|
+
.ensure_for_notarization_settings()
|
|
474
|
+
.context("macOS signing settings are not compatible with notarization")?;
|
|
475
|
+
signer
|
|
476
|
+
.write_signed_bundle(&signed_bundle_dir, &settings)
|
|
477
|
+
.with_context(|| {
|
|
478
|
+
format!(
|
|
479
|
+
"Could not write signed macOS bundle to {}",
|
|
480
|
+
signed_bundle_dir.display()
|
|
481
|
+
)
|
|
482
|
+
})?;
|
|
483
|
+
} else {
|
|
484
|
+
signer
|
|
485
|
+
.write_signed_bundle(&signed_bundle_dir, &settings)
|
|
486
|
+
.with_context(|| {
|
|
487
|
+
format!(
|
|
488
|
+
"Could not write signed macOS bundle to {}",
|
|
489
|
+
signed_bundle_dir.display()
|
|
490
|
+
)
|
|
491
|
+
})?;
|
|
492
|
+
}
|
|
421
493
|
|
|
422
494
|
Ok(())
|
|
423
495
|
})();
|
|
@@ -441,10 +513,17 @@ fn execute_macos_signing(report: &PackageReport) -> Result<()> {
|
|
|
441
513
|
Ok(())
|
|
442
514
|
}
|
|
443
515
|
|
|
444
|
-
fn macos_signing_settings(report: &PackageReport) -> Result<SigningSettings<'
|
|
516
|
+
fn macos_signing_settings<'key>(report: &PackageReport) -> Result<SigningSettings<'key>> {
|
|
445
517
|
let sign = &report.signing.macos.sign;
|
|
446
518
|
let mut settings = SigningSettings::default();
|
|
447
519
|
settings.set_binary_identifier(SettingsScope::Main, &report.metadata.bundle_identifier);
|
|
520
|
+
settings.set_for_notarization(sign.for_notarization);
|
|
521
|
+
|
|
522
|
+
if let Some(timestamp_url) = &sign.timestamp_url {
|
|
523
|
+
settings
|
|
524
|
+
.set_time_stamp_url(timestamp_url)
|
|
525
|
+
.with_context(|| format!("Invalid macOS signing timestamp URL: {timestamp_url}"))?;
|
|
526
|
+
}
|
|
448
527
|
|
|
449
528
|
if sign.hardened_runtime.unwrap_or(false) {
|
|
450
529
|
settings.add_code_signature_flags(SettingsScope::Main, CodeSignatureFlags::RUNTIME);
|
|
@@ -471,6 +550,37 @@ fn macos_signing_settings(report: &PackageReport) -> Result<SigningSettings<'sta
|
|
|
471
550
|
Ok(settings)
|
|
472
551
|
}
|
|
473
552
|
|
|
553
|
+
fn macos_p12_password(sign: &MacosSignPlan) -> Result<String> {
|
|
554
|
+
if let Some(password) = sign.p12_password.as_deref() {
|
|
555
|
+
return Ok(password.to_string());
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if let Some(env_name) = &sign.p12_password_env {
|
|
559
|
+
return std::env::var(env_name)
|
|
560
|
+
.with_context(|| format!("Could not read macOS signing p12 password env {env_name}"));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if let Some(path) = &sign.p12_password_file {
|
|
564
|
+
let password_path = Path::new(path.as_str());
|
|
565
|
+
return fs::read_to_string(password_path)
|
|
566
|
+
.with_context(|| {
|
|
567
|
+
format!(
|
|
568
|
+
"Could not read macOS signing p12 password file {}",
|
|
569
|
+
password_path.display()
|
|
570
|
+
)
|
|
571
|
+
})
|
|
572
|
+
.and_then(|contents| {
|
|
573
|
+
contents
|
|
574
|
+
.lines()
|
|
575
|
+
.next()
|
|
576
|
+
.map(str::to_string)
|
|
577
|
+
.context("macOS signing p12 password file is empty")
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
Ok(String::new())
|
|
582
|
+
}
|
|
583
|
+
|
|
474
584
|
fn print_report(report: &PackageReport, json: bool) -> Result<()> {
|
|
475
585
|
if json {
|
|
476
586
|
return output::json(report);
|
|
@@ -507,6 +617,18 @@ fn print_report(report: &PackageReport, json: bool) -> Result<()> {
|
|
|
507
617
|
if let Some(identity) = &report.signing.macos.sign.identity {
|
|
508
618
|
println!(" identity: {identity}");
|
|
509
619
|
}
|
|
620
|
+
if let Some(path) = &report.signing.macos.sign.p12_file {
|
|
621
|
+
println!(" p12 file: {path}");
|
|
622
|
+
}
|
|
623
|
+
if let Some(source) = &report.signing.macos.sign.p12_password_source {
|
|
624
|
+
println!(" p12 password: {source}");
|
|
625
|
+
}
|
|
626
|
+
if let Some(timestamp_url) = &report.signing.macos.sign.timestamp_url {
|
|
627
|
+
println!(" timestamp server: {timestamp_url}");
|
|
628
|
+
}
|
|
629
|
+
if report.signing.macos.sign.for_notarization {
|
|
630
|
+
println!(" signing mode: notarization-compatible");
|
|
631
|
+
}
|
|
510
632
|
if let Some(method) = &report.signing.macos.sign.method {
|
|
511
633
|
println!(" signing method: {method}");
|
|
512
634
|
}
|
|
@@ -648,6 +770,32 @@ fn parse_macos_sign_config(value: Option<&JsonValue>) -> MacosSignConfig {
|
|
|
648
770
|
.or_else(|| object.get("identityName"))
|
|
649
771
|
.and_then(JsonValue::as_str)
|
|
650
772
|
.map(ToOwned::to_owned),
|
|
773
|
+
p12_file: object
|
|
774
|
+
.get("p12File")
|
|
775
|
+
.or_else(|| object.get("pfxFile"))
|
|
776
|
+
.and_then(JsonValue::as_str)
|
|
777
|
+
.map(ToOwned::to_owned),
|
|
778
|
+
p12_password: object
|
|
779
|
+
.get("p12Password")
|
|
780
|
+
.or_else(|| object.get("pfxPassword"))
|
|
781
|
+
.and_then(JsonValue::as_str)
|
|
782
|
+
.map(ToOwned::to_owned),
|
|
783
|
+
p12_password_env: object
|
|
784
|
+
.get("p12PasswordEnv")
|
|
785
|
+
.or_else(|| object.get("pfxPasswordEnv"))
|
|
786
|
+
.and_then(JsonValue::as_str)
|
|
787
|
+
.map(ToOwned::to_owned),
|
|
788
|
+
p12_password_file: object
|
|
789
|
+
.get("p12PasswordFile")
|
|
790
|
+
.or_else(|| object.get("pfxPasswordFile"))
|
|
791
|
+
.and_then(JsonValue::as_str)
|
|
792
|
+
.map(ToOwned::to_owned),
|
|
793
|
+
timestamp: parse_macos_timestamp_config(
|
|
794
|
+
object
|
|
795
|
+
.get("timestamp")
|
|
796
|
+
.or_else(|| object.get("timestampUrl"))
|
|
797
|
+
.or_else(|| object.get("timestampURL")),
|
|
798
|
+
),
|
|
651
799
|
entitlements,
|
|
652
800
|
entitlements_inherit: object
|
|
653
801
|
.get("entitlementsInherit")
|
|
@@ -665,6 +813,22 @@ fn parse_macos_sign_config(value: Option<&JsonValue>) -> MacosSignConfig {
|
|
|
665
813
|
}
|
|
666
814
|
}
|
|
667
815
|
|
|
816
|
+
fn parse_macos_timestamp_config(value: Option<&JsonValue>) -> Option<MacosTimestampConfig> {
|
|
817
|
+
match value {
|
|
818
|
+
Some(JsonValue::String(value)) => {
|
|
819
|
+
let value = value.trim();
|
|
820
|
+
if value.is_empty() || value.eq_ignore_ascii_case("none") {
|
|
821
|
+
Some(MacosTimestampConfig::Disabled)
|
|
822
|
+
} else {
|
|
823
|
+
Some(MacosTimestampConfig::Url(value.to_string()))
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
Some(JsonValue::Bool(true)) => Some(MacosTimestampConfig::Default),
|
|
827
|
+
Some(JsonValue::Bool(false)) => Some(MacosTimestampConfig::Disabled),
|
|
828
|
+
_ => None,
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
668
832
|
fn parse_macos_notarize_config(value: Option<&JsonValue>) -> MacosNotarizeConfig {
|
|
669
833
|
match value {
|
|
670
834
|
None => MacosNotarizeConfig::default(),
|
|
@@ -793,7 +957,13 @@ fn package_signing(
|
|
|
793
957
|
platform: &str,
|
|
794
958
|
) -> Result<(PackageSigningPlan, Vec<String>)> {
|
|
795
959
|
let mut warnings = Vec::new();
|
|
796
|
-
let sign = macos_sign_plan(
|
|
960
|
+
let sign = macos_sign_plan(
|
|
961
|
+
root,
|
|
962
|
+
&config.packager.osx_sign,
|
|
963
|
+
&config.packager.osx_notarize,
|
|
964
|
+
platform,
|
|
965
|
+
&mut warnings,
|
|
966
|
+
)?;
|
|
797
967
|
let notarize = macos_notarize_plan(root, config, platform, &mut warnings);
|
|
798
968
|
|
|
799
969
|
Ok((
|
|
@@ -807,6 +977,7 @@ fn package_signing(
|
|
|
807
977
|
fn macos_sign_plan(
|
|
808
978
|
root: &Path,
|
|
809
979
|
config: &MacosSignConfig,
|
|
980
|
+
notarize_config: &MacosNotarizeConfig,
|
|
810
981
|
platform: &str,
|
|
811
982
|
warnings: &mut Vec<String>,
|
|
812
983
|
) -> Result<MacosSignPlan> {
|
|
@@ -844,11 +1015,63 @@ fn macos_sign_plan(
|
|
|
844
1015
|
}
|
|
845
1016
|
}
|
|
846
1017
|
|
|
1018
|
+
let p12_file = config
|
|
1019
|
+
.p12_file
|
|
1020
|
+
.as_deref()
|
|
1021
|
+
.filter(|path| !path.trim().is_empty())
|
|
1022
|
+
.map(|path| utf8_path(resolve_project_path(root, path)))
|
|
1023
|
+
.transpose()?;
|
|
1024
|
+
if let Some(path) = &p12_file {
|
|
1025
|
+
if !Path::new(path.as_str()).exists() {
|
|
1026
|
+
warnings.push(format!(
|
|
1027
|
+
"Configured macOS signing p12 file does not exist: {}.",
|
|
1028
|
+
path
|
|
1029
|
+
));
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
let p12_password_file = config
|
|
1033
|
+
.p12_password_file
|
|
1034
|
+
.as_deref()
|
|
1035
|
+
.filter(|path| !path.trim().is_empty())
|
|
1036
|
+
.map(|path| utf8_path(resolve_project_path(root, path)))
|
|
1037
|
+
.transpose()?;
|
|
1038
|
+
if let Some(path) = &p12_password_file {
|
|
1039
|
+
if !Path::new(path.as_str()).exists() {
|
|
1040
|
+
warnings.push(format!(
|
|
1041
|
+
"Configured macOS signing p12 password file does not exist: {}.",
|
|
1042
|
+
path
|
|
1043
|
+
));
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
let p12_password_source = if p12_file.is_some() {
|
|
1047
|
+
if config.p12_password.is_some() {
|
|
1048
|
+
Some("config".to_string())
|
|
1049
|
+
} else if let Some(env_name) = config
|
|
1050
|
+
.p12_password_env
|
|
1051
|
+
.as_deref()
|
|
1052
|
+
.filter(|name| !name.trim().is_empty())
|
|
1053
|
+
{
|
|
1054
|
+
Some(format!("env:{env_name}"))
|
|
1055
|
+
} else if let Some(path) = &p12_password_file {
|
|
1056
|
+
Some(format!("file:{path}"))
|
|
1057
|
+
} else {
|
|
1058
|
+
Some("empty".to_string())
|
|
1059
|
+
}
|
|
1060
|
+
} else {
|
|
1061
|
+
None
|
|
1062
|
+
};
|
|
1063
|
+
|
|
847
1064
|
let identity = config.identity.as_deref().map(str::trim);
|
|
848
1065
|
let ad_hoc_identity = matches!(identity, None | Some("-"));
|
|
849
|
-
let
|
|
1066
|
+
let p12_identity = p12_file.is_some();
|
|
1067
|
+
let will_execute = config.enabled && platform == "darwin" && (ad_hoc_identity || p12_identity);
|
|
1068
|
+
let timestamp_url = macos_timestamp_url(config, p12_identity, notarize_config.enabled);
|
|
1069
|
+
let for_notarization =
|
|
1070
|
+
will_execute && p12_identity && notarize_config.enabled && timestamp_url.is_some();
|
|
850
1071
|
let method = if config.enabled && platform == "darwin" {
|
|
851
|
-
if
|
|
1072
|
+
if p12_identity {
|
|
1073
|
+
Some("certificate-p12".to_string())
|
|
1074
|
+
} else if ad_hoc_identity {
|
|
852
1075
|
Some("ad-hoc".to_string())
|
|
853
1076
|
} else {
|
|
854
1077
|
Some("certificate-identity".to_string())
|
|
@@ -863,17 +1086,22 @@ fn macos_sign_plan(
|
|
|
863
1086
|
));
|
|
864
1087
|
} else if config.enabled && !will_execute {
|
|
865
1088
|
warnings.push(
|
|
866
|
-
"macOS signing identity is configured, but Rust-native
|
|
1089
|
+
"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
1090
|
);
|
|
868
1091
|
} else if will_execute {
|
|
1092
|
+
if p12_identity && identity.is_some() {
|
|
1093
|
+
warnings.push(
|
|
1094
|
+
"packagerConfig.osxSign.p12File supplies the signing certificate; identity is reported but not used for keychain lookup.".to_string(),
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
869
1097
|
if config.entitlements.len() > 1 {
|
|
870
1098
|
warnings.push(
|
|
871
|
-
"Rust-native
|
|
1099
|
+
"Rust-native macOS signing applies the first macOS entitlements file only; inherited/login-helper entitlement scoping is not implemented yet.".to_string(),
|
|
872
1100
|
);
|
|
873
1101
|
}
|
|
874
1102
|
if config.entitlements_inherit.is_some() {
|
|
875
1103
|
warnings.push(
|
|
876
|
-
"packagerConfig.osxSign.entitlementsInherit is recognized but not applied to nested bundles by Rust-native
|
|
1104
|
+
"packagerConfig.osxSign.entitlementsInherit is recognized but not applied to nested bundles by Rust-native signing yet.".to_string(),
|
|
877
1105
|
);
|
|
878
1106
|
}
|
|
879
1107
|
if config.gatekeeper_assess.is_some() {
|
|
@@ -881,6 +1109,16 @@ fn macos_sign_plan(
|
|
|
881
1109
|
"packagerConfig.osxSign.gatekeeperAssess is recognized but Gatekeeper assessment is not implemented yet.".to_string(),
|
|
882
1110
|
);
|
|
883
1111
|
}
|
|
1112
|
+
if config.timestamp.is_some() && !p12_identity {
|
|
1113
|
+
warnings.push(
|
|
1114
|
+
"packagerConfig.osxSign.timestamp is recognized but ignored without p12File certificate signing.".to_string(),
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
if notarize_config.enabled && p12_identity && timestamp_url.is_none() {
|
|
1118
|
+
warnings.push(
|
|
1119
|
+
"macOS notarization requires a secure timestamp; packagerConfig.osxSign.timestamp disabled timestamping.".to_string(),
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
884
1122
|
}
|
|
885
1123
|
|
|
886
1124
|
Ok(MacosSignPlan {
|
|
@@ -889,6 +1127,13 @@ fn macos_sign_plan(
|
|
|
889
1127
|
will_execute,
|
|
890
1128
|
method,
|
|
891
1129
|
identity: config.identity.clone(),
|
|
1130
|
+
p12_file,
|
|
1131
|
+
p12_password_source,
|
|
1132
|
+
p12_password_env: config.p12_password_env.clone(),
|
|
1133
|
+
p12_password_file,
|
|
1134
|
+
p12_password: RedactedSecret::new(config.p12_password.clone()),
|
|
1135
|
+
timestamp_url,
|
|
1136
|
+
for_notarization,
|
|
892
1137
|
entitlements,
|
|
893
1138
|
entitlements_inherit,
|
|
894
1139
|
hardened_runtime: config.hardened_runtime,
|
|
@@ -896,6 +1141,24 @@ fn macos_sign_plan(
|
|
|
896
1141
|
})
|
|
897
1142
|
}
|
|
898
1143
|
|
|
1144
|
+
fn macos_timestamp_url(
|
|
1145
|
+
config: &MacosSignConfig,
|
|
1146
|
+
p12_identity: bool,
|
|
1147
|
+
notarize_enabled: bool,
|
|
1148
|
+
) -> Option<String> {
|
|
1149
|
+
if !p12_identity {
|
|
1150
|
+
return None;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
match &config.timestamp {
|
|
1154
|
+
Some(MacosTimestampConfig::Default) => Some(APPLE_TIMESTAMP_URL.to_string()),
|
|
1155
|
+
Some(MacosTimestampConfig::Disabled) => None,
|
|
1156
|
+
Some(MacosTimestampConfig::Url(url)) => Some(url.clone()),
|
|
1157
|
+
None if notarize_enabled => Some(APPLE_TIMESTAMP_URL.to_string()),
|
|
1158
|
+
None => None,
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
899
1162
|
fn macos_notarize_plan(
|
|
900
1163
|
root: &Path,
|
|
901
1164
|
package_config: &PackageJsonConfig,
|
|
@@ -926,6 +1189,7 @@ fn macos_notarize_plan(
|
|
|
926
1189
|
if config.enabled
|
|
927
1190
|
&& platform == "darwin"
|
|
928
1191
|
&& package_config.packager.osx_sign.enabled
|
|
1192
|
+
&& package_config.packager.osx_sign.p12_file.is_none()
|
|
929
1193
|
&& matches!(
|
|
930
1194
|
package_config
|
|
931
1195
|
.packager
|
|
@@ -2008,7 +2272,7 @@ mod tests {
|
|
|
2008
2272
|
assert!(report
|
|
2009
2273
|
.warnings
|
|
2010
2274
|
.iter()
|
|
2011
|
-
.any(|warning| warning.contains("Rust-native
|
|
2275
|
+
.any(|warning| warning.contains("Rust-native keychain identity signing")));
|
|
2012
2276
|
assert!(report
|
|
2013
2277
|
.warnings
|
|
2014
2278
|
.iter()
|
|
@@ -2063,13 +2327,198 @@ mod tests {
|
|
|
2063
2327
|
assert_eq!(report.signing.macos.sign.identity.as_deref(), Some("-"));
|
|
2064
2328
|
assert_eq!(report.signing.macos.sign.hardened_runtime, Some(true));
|
|
2065
2329
|
assert!(!report.warnings.iter().any(|warning| {
|
|
2066
|
-
warning.contains("Rust-native
|
|
2330
|
+
warning.contains("Rust-native keychain identity signing")
|
|
2067
2331
|
|| warning.contains("Rust-native signing is not implemented")
|
|
2068
2332
|
}));
|
|
2069
2333
|
|
|
2070
2334
|
let _ = fs::remove_dir_all(root);
|
|
2071
2335
|
}
|
|
2072
2336
|
|
|
2337
|
+
#[test]
|
|
2338
|
+
fn plans_macos_p12_signing_without_serializing_password() {
|
|
2339
|
+
let root = unique_temp_dir("macos-p12-signing-plan");
|
|
2340
|
+
write_package_json(&root);
|
|
2341
|
+
fs::write(root.join("developer-id.p12"), "not a real p12")
|
|
2342
|
+
.expect("p12 placeholder should be written");
|
|
2343
|
+
fs::write(
|
|
2344
|
+
root.join("forge.config.js"),
|
|
2345
|
+
r#"
|
|
2346
|
+
module.exports = {
|
|
2347
|
+
packagerConfig: {
|
|
2348
|
+
osxSign: {
|
|
2349
|
+
identity: 'Developer ID Application: Example, Inc. (TEAMID1234)',
|
|
2350
|
+
p12File: 'developer-id.p12',
|
|
2351
|
+
p12Password: 'p12-secret',
|
|
2352
|
+
timestamp: 'http://timestamp.example.test/tsa',
|
|
2353
|
+
hardenedRuntime: true,
|
|
2354
|
+
},
|
|
2355
|
+
},
|
|
2356
|
+
};
|
|
2357
|
+
"#,
|
|
2358
|
+
)
|
|
2359
|
+
.expect("forge config should be written");
|
|
2360
|
+
write_app_file(&root);
|
|
2361
|
+
write_fake_electron_dist(&root);
|
|
2362
|
+
|
|
2363
|
+
let args = PackageArgs {
|
|
2364
|
+
cwd: root.clone(),
|
|
2365
|
+
out_dir: PathBuf::from("out"),
|
|
2366
|
+
name: None,
|
|
2367
|
+
platform: Some("darwin".to_string()),
|
|
2368
|
+
arch: Some("arm64".to_string()),
|
|
2369
|
+
force: false,
|
|
2370
|
+
dry_run: true,
|
|
2371
|
+
json: true,
|
|
2372
|
+
};
|
|
2373
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2374
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2375
|
+
|
|
2376
|
+
assert!(report.signing.macos.sign.configured);
|
|
2377
|
+
assert!(report.signing.macos.sign.enabled);
|
|
2378
|
+
assert!(report.signing.macos.sign.will_execute);
|
|
2379
|
+
assert_eq!(
|
|
2380
|
+
report.signing.macos.sign.method.as_deref(),
|
|
2381
|
+
Some("certificate-p12")
|
|
2382
|
+
);
|
|
2383
|
+
assert_eq!(
|
|
2384
|
+
report.signing.macos.sign.p12_password_source.as_deref(),
|
|
2385
|
+
Some("config")
|
|
2386
|
+
);
|
|
2387
|
+
assert_eq!(
|
|
2388
|
+
report.signing.macos.sign.timestamp_url.as_deref(),
|
|
2389
|
+
Some("http://timestamp.example.test/tsa")
|
|
2390
|
+
);
|
|
2391
|
+
assert!(!report.signing.macos.sign.for_notarization);
|
|
2392
|
+
assert!(report.signing.macos.sign.p12_file.is_some());
|
|
2393
|
+
assert!(report
|
|
2394
|
+
.warnings
|
|
2395
|
+
.iter()
|
|
2396
|
+
.any(|warning| { warning.contains("p12File supplies the signing certificate") }));
|
|
2397
|
+
|
|
2398
|
+
let json = serde_json::to_string(&report).expect("report should serialize");
|
|
2399
|
+
assert!(!json.contains("p12-secret"));
|
|
2400
|
+
|
|
2401
|
+
let _ = fs::remove_dir_all(root);
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
#[test]
|
|
2405
|
+
fn plans_macos_p12_signing_for_notarization_with_default_timestamp() {
|
|
2406
|
+
let root = unique_temp_dir("macos-p12-notarization-signing-plan");
|
|
2407
|
+
write_package_json(&root);
|
|
2408
|
+
fs::write(root.join("developer-id.p12"), "not a real p12")
|
|
2409
|
+
.expect("p12 placeholder should be written");
|
|
2410
|
+
fs::write(
|
|
2411
|
+
root.join("forge.config.js"),
|
|
2412
|
+
r#"
|
|
2413
|
+
module.exports = {
|
|
2414
|
+
packagerConfig: {
|
|
2415
|
+
appBundleId: 'com.example.notarized',
|
|
2416
|
+
osxSign: {
|
|
2417
|
+
p12File: 'developer-id.p12',
|
|
2418
|
+
p12PasswordEnv: 'P12_PASSWORD',
|
|
2419
|
+
},
|
|
2420
|
+
osxNotarize: {
|
|
2421
|
+
keychainProfile: 'notary-profile',
|
|
2422
|
+
},
|
|
2423
|
+
},
|
|
2424
|
+
};
|
|
2425
|
+
"#,
|
|
2426
|
+
)
|
|
2427
|
+
.expect("forge config should be written");
|
|
2428
|
+
write_app_file(&root);
|
|
2429
|
+
write_fake_electron_dist(&root);
|
|
2430
|
+
|
|
2431
|
+
let args = PackageArgs {
|
|
2432
|
+
cwd: root.clone(),
|
|
2433
|
+
out_dir: PathBuf::from("out"),
|
|
2434
|
+
name: None,
|
|
2435
|
+
platform: Some("darwin".to_string()),
|
|
2436
|
+
arch: Some("arm64".to_string()),
|
|
2437
|
+
force: false,
|
|
2438
|
+
dry_run: true,
|
|
2439
|
+
json: true,
|
|
2440
|
+
};
|
|
2441
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2442
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2443
|
+
|
|
2444
|
+
assert!(report.signing.macos.sign.will_execute);
|
|
2445
|
+
assert_eq!(
|
|
2446
|
+
report.signing.macos.sign.timestamp_url.as_deref(),
|
|
2447
|
+
Some(APPLE_TIMESTAMP_URL)
|
|
2448
|
+
);
|
|
2449
|
+
assert!(report.signing.macos.sign.for_notarization);
|
|
2450
|
+
assert_eq!(
|
|
2451
|
+
report.signing.macos.notarize.auth_method.as_deref(),
|
|
2452
|
+
Some("keychain-profile")
|
|
2453
|
+
);
|
|
2454
|
+
assert!(!report
|
|
2455
|
+
.warnings
|
|
2456
|
+
.iter()
|
|
2457
|
+
.any(|warning| warning.contains("ad-hoc signing is not notarizable")));
|
|
2458
|
+
|
|
2459
|
+
let settings = macos_signing_settings(&report).expect("signing settings should build");
|
|
2460
|
+
assert!(settings.for_notarization());
|
|
2461
|
+
assert_eq!(
|
|
2462
|
+
settings.time_stamp_url().map(|url| url.as_str()),
|
|
2463
|
+
Some(APPLE_TIMESTAMP_URL)
|
|
2464
|
+
);
|
|
2465
|
+
|
|
2466
|
+
let json = serde_json::to_string(&report).expect("report should serialize");
|
|
2467
|
+
assert!(!json.contains("P12_PASSWORD="));
|
|
2468
|
+
|
|
2469
|
+
let _ = fs::remove_dir_all(root);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
#[test]
|
|
2473
|
+
fn warns_when_macos_notarization_timestamp_is_disabled() {
|
|
2474
|
+
let root = unique_temp_dir("macos-p12-notarization-no-timestamp");
|
|
2475
|
+
write_package_json(&root);
|
|
2476
|
+
fs::write(root.join("developer-id.p12"), "not a real p12")
|
|
2477
|
+
.expect("p12 placeholder should be written");
|
|
2478
|
+
fs::write(
|
|
2479
|
+
root.join("forge.config.js"),
|
|
2480
|
+
r#"
|
|
2481
|
+
module.exports = {
|
|
2482
|
+
packagerConfig: {
|
|
2483
|
+
osxSign: {
|
|
2484
|
+
p12File: 'developer-id.p12',
|
|
2485
|
+
timestamp: 'none',
|
|
2486
|
+
},
|
|
2487
|
+
osxNotarize: {
|
|
2488
|
+
keychainProfile: 'notary-profile',
|
|
2489
|
+
},
|
|
2490
|
+
},
|
|
2491
|
+
};
|
|
2492
|
+
"#,
|
|
2493
|
+
)
|
|
2494
|
+
.expect("forge config should be written");
|
|
2495
|
+
write_app_file(&root);
|
|
2496
|
+
write_fake_electron_dist(&root);
|
|
2497
|
+
|
|
2498
|
+
let args = PackageArgs {
|
|
2499
|
+
cwd: root.clone(),
|
|
2500
|
+
out_dir: PathBuf::from("out"),
|
|
2501
|
+
name: None,
|
|
2502
|
+
platform: Some("darwin".to_string()),
|
|
2503
|
+
arch: Some("arm64".to_string()),
|
|
2504
|
+
force: false,
|
|
2505
|
+
dry_run: true,
|
|
2506
|
+
json: true,
|
|
2507
|
+
};
|
|
2508
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2509
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2510
|
+
|
|
2511
|
+
assert!(report.signing.macos.sign.will_execute);
|
|
2512
|
+
assert!(report.signing.macos.sign.timestamp_url.is_none());
|
|
2513
|
+
assert!(!report.signing.macos.sign.for_notarization);
|
|
2514
|
+
assert!(report
|
|
2515
|
+
.warnings
|
|
2516
|
+
.iter()
|
|
2517
|
+
.any(|warning| warning.contains("requires a secure timestamp")));
|
|
2518
|
+
|
|
2519
|
+
let _ = fs::remove_dir_all(root);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2073
2522
|
#[test]
|
|
2074
2523
|
fn warns_when_macos_notarization_is_configured_without_signing() {
|
|
2075
2524
|
let root = unique_temp_dir("notarize-without-sign");
|
|
@@ -2252,6 +2701,69 @@ mod tests {
|
|
|
2252
2701
|
let _ = fs::remove_dir_all(root);
|
|
2253
2702
|
}
|
|
2254
2703
|
|
|
2704
|
+
#[test]
|
|
2705
|
+
fn packages_macos_bundle_with_p12_certificate_signature() {
|
|
2706
|
+
if current_platform() != "darwin" {
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
let Some(p12_fixture) = apple_codesign_test_fixture("apple-codesign-testuser.p12") else {
|
|
2711
|
+
return;
|
|
2712
|
+
};
|
|
2713
|
+
|
|
2714
|
+
let root = unique_temp_dir("macos-p12-signing-execute");
|
|
2715
|
+
fs::copy(&p12_fixture, root.join("developer-id.p12"))
|
|
2716
|
+
.expect("p12 fixture should be copied");
|
|
2717
|
+
fs::write(
|
|
2718
|
+
root.join("package.json"),
|
|
2719
|
+
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}}}}"#,
|
|
2720
|
+
)
|
|
2721
|
+
.expect("package.json should be written");
|
|
2722
|
+
write_app_file(&root);
|
|
2723
|
+
write_macho_electron_dist(&root);
|
|
2724
|
+
|
|
2725
|
+
let args = PackageArgs {
|
|
2726
|
+
cwd: root.clone(),
|
|
2727
|
+
out_dir: PathBuf::from("out"),
|
|
2728
|
+
name: None,
|
|
2729
|
+
platform: None,
|
|
2730
|
+
arch: None,
|
|
2731
|
+
force: false,
|
|
2732
|
+
dry_run: false,
|
|
2733
|
+
json: false,
|
|
2734
|
+
};
|
|
2735
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2736
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2737
|
+
|
|
2738
|
+
assert!(report.signing.macos.sign.will_execute);
|
|
2739
|
+
assert_eq!(
|
|
2740
|
+
report.signing.macos.sign.method.as_deref(),
|
|
2741
|
+
Some("certificate-p12")
|
|
2742
|
+
);
|
|
2743
|
+
assert!(report.warnings.is_empty());
|
|
2744
|
+
|
|
2745
|
+
execute_package(&report, false).expect("package should succeed");
|
|
2746
|
+
|
|
2747
|
+
let executable = Path::new(report.bundle_dir.as_str())
|
|
2748
|
+
.join("Contents/MacOS")
|
|
2749
|
+
.join(&report.executable_name);
|
|
2750
|
+
let executable_data = fs::read(executable).expect("signed executable should read");
|
|
2751
|
+
let macho = apple_codesign::MachFile::parse(&executable_data)
|
|
2752
|
+
.expect("signed executable should parse as Mach-O");
|
|
2753
|
+
assert!(macho.iter_macho().all(|binary| {
|
|
2754
|
+
let signature = binary
|
|
2755
|
+
.code_signature()
|
|
2756
|
+
.expect("code signature should parse")
|
|
2757
|
+
.expect("code signature should exist");
|
|
2758
|
+
signature
|
|
2759
|
+
.signature_data()
|
|
2760
|
+
.expect("CMS signature should parse")
|
|
2761
|
+
.is_some_and(|data| !data.is_empty())
|
|
2762
|
+
}));
|
|
2763
|
+
|
|
2764
|
+
let _ = fs::remove_dir_all(root);
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2255
2767
|
#[test]
|
|
2256
2768
|
fn missing_required_runtime_dependency_fails() {
|
|
2257
2769
|
let root = unique_temp_dir("runtime-deps");
|
|
@@ -2415,6 +2927,31 @@ mod tests {
|
|
|
2415
2927
|
.expect("Mach-O test executable should be copied");
|
|
2416
2928
|
}
|
|
2417
2929
|
|
|
2930
|
+
fn apple_codesign_test_fixture(file_name: &str) -> Option<PathBuf> {
|
|
2931
|
+
let cargo_home = std::env::var_os("CARGO_HOME")
|
|
2932
|
+
.map(PathBuf::from)
|
|
2933
|
+
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".cargo")))?;
|
|
2934
|
+
let registry_src = cargo_home.join("registry/src");
|
|
2935
|
+
for index_dir in fs::read_dir(registry_src).ok()? {
|
|
2936
|
+
let index_dir = index_dir.ok()?;
|
|
2937
|
+
for crate_dir in fs::read_dir(index_dir.path()).ok()? {
|
|
2938
|
+
let crate_dir = crate_dir.ok()?;
|
|
2939
|
+
let file_name_matches = crate_dir
|
|
2940
|
+
.file_name()
|
|
2941
|
+
.to_str()
|
|
2942
|
+
.is_some_and(|name| name.starts_with("apple-codesign-"));
|
|
2943
|
+
if file_name_matches {
|
|
2944
|
+
let candidate = crate_dir.path().join("src").join(file_name);
|
|
2945
|
+
if candidate.exists() {
|
|
2946
|
+
return Some(candidate);
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
None
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2418
2955
|
fn unique_temp_dir(label: &str) -> PathBuf {
|
|
2419
2956
|
let nanos = std::time::SystemTime::now()
|
|
2420
2957
|
.duration_since(std::time::UNIX_EPOCH)
|