Skip to content

Commit 3070c19

Browse files
Windows: show installer failure stages and native errors for Agent 0.0.15 (#1371)
* Windows: report installer failures and prepare Agent 0.0.15 * Windows: keep installer diagnostics in the existing error dialog * Windows: keep diagnostics scoped to the existing installer checks * Windows: ensure installer metadata cannot overwrite the child status * Windows: share installer errors with the standalone signature probe
1 parent 73e84a9 commit 3070c19

10 files changed

Lines changed: 355 additions & 67 deletions

File tree

‎apps/maple-agent/Cargo.lock‎

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎apps/maple-agent/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ members = ["crates/maple-agent", "crates/maple-billing", "crates/maple-agent-upd
44

55
[workspace.package]
66
edition = "2024"
7-
version = "0.0.14"
7+
version = "0.0.15"
88
license = "MIT"
99

1010
[workspace.dependencies]

‎apps/maple-agent/app/src/main.rs‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,10 @@ fn main() {
296296
root,
297297
profile::PROFILE,
298298
)
299-
.map_err(|error| error.to_string())
299+
.map_err(|error| {
300+
internal_exit_code = error.windows_installer_exit_code();
301+
error.to_string()
302+
})
300303
}),
301304
)
302305
} else if !cli.internal_windows_installer_prepare.is_empty() {
@@ -315,7 +318,10 @@ fn main() {
315318
profile::PROFILE,
316319
generation,
317320
)
318-
.map_err(|error| error.to_string())
321+
.map_err(|error| {
322+
internal_exit_code = error.windows_installer_exit_code();
323+
error.to_string()
324+
})
319325
}),
320326
)
321327
} else if let Some(generation) = &cli.internal_windows_installer_activate {

‎apps/maple-agent/crates/maple-agent-updater/examples/windows_signature_probe.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
//! Manual, read-only historical Authenticode interoperability probe. The selected
22
//! executable is never launched; this is not installed-update qualification.
33
4+
#[cfg(windows)]
5+
use maple_agent_updater::installer::InstallError;
6+
#[cfg(windows)]
7+
type Result<T> = std::result::Result<T, InstallError>;
8+
49
#[cfg(windows)]
510
#[allow(dead_code)]
611
#[path = "../src/installer/windows_bundle/authenticode.rs"]

‎apps/maple-agent/crates/maple-agent-updater/src/installer.rs‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ pub enum InstallError {
105105
Invalid(&'static str),
106106
#[error("The update does not have the required Maple code signature and identity")]
107107
CodeSignature,
108+
#[error("Windows could not verify Maple's code signature (WinTrust error 0x{status:08X})")]
109+
WindowsCodeSignature { status: u32 },
108110
#[error("The update could not be installed: {0}")]
109111
Io(#[from] io::Error),
110112
#[error("Windows activation failed during {stage}: {source}")]
@@ -127,6 +129,26 @@ impl InstallError {
127129
}
128130
}
129131

132+
/// Report a failed preflight/preparation through NSIS without losing a
133+
/// native error's high bits. Every category remains a nonzero failure;
134+
/// activation keeps its existing separately decoded stage contract.
135+
pub fn windows_installer_exit_code(&self) -> i32 {
136+
match self {
137+
Self::WindowsCodeSignature { status } if *status != 0 => *status as i32,
138+
Self::CodeSignature | Self::WindowsCodeSignature { .. } => 0x4E00_0001,
139+
Self::Busy => 0x4E00_0002,
140+
Self::Invalid(_) => 0x4E00_0003,
141+
Self::NotifyOnly(_) => 0x4E00_0004,
142+
Self::Io(error) => error
143+
.raw_os_error()
144+
.filter(|code| *code != 0)
145+
.unwrap_or(0x4E00_0005),
146+
Self::WindowsActivation { .. } => {
147+
self.windows_activation_exit_code().unwrap_or(0x4E00_0003)
148+
}
149+
}
150+
}
151+
130152
/// Encode only an activation failure, without paths, SIDs or error text.
131153
/// The original typed source remains available to direct callers.
132154
pub fn windows_activation_exit_code(&self) -> Option<i32> {
@@ -150,6 +172,92 @@ type Result<T> = std::result::Result<T, InstallError>;
150172
mod windows_activation_error_tests {
151173
use super::*;
152174

175+
#[test]
176+
fn installer_status_retains_full_native_bits_including_negative_trust_errors() {
177+
for status in [1, 0x800B_0109, 0x8009_2013, u32::MAX] {
178+
let error = InstallError::WindowsCodeSignature { status };
179+
let code = error.windows_installer_exit_code();
180+
assert_eq!(code as u32, status);
181+
assert_ne!(code, 0);
182+
}
183+
for code in [5, 32, 65_536, i32::MIN, i32::MAX, -1] {
184+
let error = InstallError::Io(io::Error::from_raw_os_error(code));
185+
assert_eq!(error.windows_installer_exit_code(), code);
186+
}
187+
}
188+
189+
#[test]
190+
fn installer_status_fallbacks_never_return_success_or_change_activation_status() {
191+
for (error, code) in [
192+
(InstallError::CodeSignature, 0x4E00_0001),
193+
(
194+
InstallError::WindowsCodeSignature { status: 0 },
195+
0x4E00_0001,
196+
),
197+
(InstallError::Busy, 0x4E00_0002),
198+
(InstallError::Invalid("public fixture"), 0x4E00_0003),
199+
(InstallError::NotifyOnly("public fixture"), 0x4E00_0004),
200+
(
201+
InstallError::Io(io::Error::from(io::ErrorKind::PermissionDenied)),
202+
0x4E00_0005,
203+
),
204+
(
205+
InstallError::Io(io::Error::from_raw_os_error(0)),
206+
0x4E00_0005,
207+
),
208+
] {
209+
assert_eq!(error.windows_installer_exit_code(), code);
210+
assert_ne!(code, 0);
211+
}
212+
for source in [
213+
InstallError::WindowsCodeSignature {
214+
status: 0x800B_0109,
215+
},
216+
InstallError::Io(io::Error::from_raw_os_error(32)),
217+
InstallError::Invalid("public fixture"),
218+
] {
219+
let error = InstallError::WindowsActivation {
220+
stage: WindowsActivationStage::GenerationVerification,
221+
source: Box::new(source),
222+
};
223+
assert_eq!(
224+
Some(error.windows_installer_exit_code()),
225+
error.windows_activation_exit_code()
226+
);
227+
assert_ne!(error.windows_installer_exit_code(), 0);
228+
}
229+
}
230+
231+
#[test]
232+
fn windows_signature_status_survives_activation_context_without_changing_error_policy() {
233+
for status in [0x800B_0109, 0x8009_2013, 0x800B_0100, u32::MAX] {
234+
let source = InstallError::WindowsCodeSignature { status };
235+
assert_eq!(
236+
source.to_string(),
237+
format!(
238+
"Windows could not verify Maple's code signature (WinTrust error 0x{status:08X})"
239+
)
240+
);
241+
assert!(!source.requires_manual_installation());
242+
let error = InstallError::WindowsActivation {
243+
stage: WindowsActivationStage::BootstrapIdentity,
244+
source: Box::new(source),
245+
};
246+
assert!(error.to_string().contains("bootstrap-identity"));
247+
assert!(error.to_string().contains(&format!("0x{status:08X}")));
248+
// The existing bounded activation status is not repurposed to
249+
// truncate or disguise a full WinTrust DWORD.
250+
assert_eq!(error.windows_activation_exit_code(), Some(0x4D04_0000));
251+
assert!(!error.requires_manual_installation());
252+
let InstallError::WindowsActivation { source, .. } = error else {
253+
unreachable!()
254+
};
255+
assert!(
256+
matches!(*source, InstallError::WindowsCodeSignature { status: actual } if actual == status)
257+
);
258+
}
259+
}
260+
153261
#[test]
154262
fn activation_status_preserves_fixed_stage_and_original_native_error() {
155263
for (stage, number, label) in [

‎apps/maple-agent/crates/maple-agent-updater/src/installer/windows_bundle.rs‎

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -452,15 +452,15 @@ fn verify_identity_metadata(directory: &Path, bundle: &Bundle) -> Result<PathBuf
452452
fn verify_identity_for(directory: &Path, bundle: &Bundle, purpose: IdentityPurpose) -> Result<()> {
453453
let executable = verify_identity_metadata(directory, bundle)?;
454454
#[cfg(windows)]
455-
if !match purpose {
455+
match purpose {
456456
IdentityPurpose::Admission => {
457-
authenticode::verify_or_unsigned(&executable, bundle.profile == "dev")
457+
authenticode::verify_or_unsigned_detailed(&executable, bundle.profile == "dev")?;
458458
}
459459
IdentityPurpose::Startup => {
460-
authenticode::verify_startup_or_unsigned(&executable, bundle.profile == "dev")
460+
if !authenticode::verify_startup_or_unsigned(&executable, bundle.profile == "dev") {
461+
return Err(InstallError::CodeSignature);
462+
}
461463
}
462-
} {
463-
return Err(InstallError::CodeSignature);
464464
}
465465
#[cfg(not(windows))]
466466
let _ = (purpose, executable);
@@ -1168,10 +1168,10 @@ fn trusted_installer(executable: &Path, profile: &str) -> Result<serde_json::Val
11681168
.as_str()
11691169
.is_some_and(|v| manifest::stable_version(v).is_ok())
11701170
|| !info["source_sha"].as_str().is_some_and(valid_source)
1171-
|| !authenticode::verify_or_unsigned(executable, profile == "dev")
11721171
{
11731172
return Err(InstallError::CodeSignature);
11741173
}
1174+
authenticode::verify_or_unsigned_detailed(executable, profile == "dev")?;
11751175
Ok(info)
11761176
}
11771177

@@ -1870,11 +1870,10 @@ pub fn prepare_windows_installation(
18701870
.tempfile_in(&root)?
18711871
.into_temp_path();
18721872
fs::copy(&checker, &temporary)?;
1873-
if exact_file_digest(&temporary)? != intent.executable_sha256
1874-
|| !authenticode::verify_or_unsigned(&temporary, profile == "dev")
1875-
{
1873+
if exact_file_digest(&temporary)? != intent.executable_sha256 {
18761874
return Err(InstallError::CodeSignature);
18771875
}
1876+
authenticode::verify_or_unsigned_detailed(&temporary, profile == "dev")?;
18781877
windows_atomic_replace(&temporary, &root.join("maple-agent.exe"))?;
18791878
}
18801879
windows_sync_directory(&root)?;
@@ -2051,11 +2050,10 @@ pub fn cleanup_windows_installation(executable: &Path, profile: &str) -> Result<
20512050
.ok_or(InstallError::Invalid("Windows uninstall parent"))?;
20522051
windows_fs::validate_path(root)?;
20532052
registered_root(root, profile)?;
2054-
if pe_build_info(&executable)?["profile"] != profile
2055-
|| !authenticode::verify_or_unsigned(&executable, profile == "dev")
2056-
{
2053+
if pe_build_info(&executable)?["profile"] != profile {
20572054
return Err(InstallError::CodeSignature);
20582055
}
2056+
authenticode::verify_or_unsigned_detailed(&executable, profile == "dev")?;
20592057
let lock = open_install_lock(root)?;
20602058
lock.try_lock_exclusive().map_err(|_| InstallError::Busy)?;
20612059
let owner = installer_owner()?;

0 commit comments

Comments
 (0)