Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 93 additions & 3 deletions src-tauri/src/commands/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,9 @@ fn effective_gpg_program(
runner: &impl VerificationRunner,
) -> Result<String, String> {
if let Ok(Some(program)) = runner.git_config_get(repo_path, "gpg.program") {
return Ok(program);
if let Some(validated) = validate_gpg_program_from_repo(&program) {
return Ok(validated);
}
}

#[cfg(windows)]
Expand All @@ -363,6 +365,52 @@ fn effective_gpg_program(
Ok("gpg".to_string())
}

/// Accepts programs resolved from `PATH` and existing absolute files.
fn validate_gpg_program_from_repo(program: &str) -> Option<String> {
let trimmed = program.trim().trim_matches('"');

if trimmed.is_empty() {
return None;
}

let is_path_like =
Path::new(trimmed).is_absolute() || trimmed.contains('/') || trimmed.contains('\\');

if is_path_like {
let path = Path::new(trimmed);
if path.is_absolute() && path.is_file() {
return Some(trimmed.to_string());
}
return None;
}

#[cfg(windows)]
let exists = {
let executable_name = format!("{trimmed}.exe");
program_exists_on_path(&[executable_name.as_str(), trimmed])
};
#[cfg(not(windows))]
let exists = program_exists_on_path(&[trimmed]);

exists.then(|| trimmed.to_string())
}

fn program_exists_on_path(names: &[&str]) -> bool {
let Some(path_var) = std::env::var_os("PATH") else {
return false;
};

for dir in std::env::split_paths(&path_var) {
for name in names {
let candidate = dir.join(name);
if candidate.exists() && candidate.is_file() {
return true;
}
}
}
false
}

fn signature_key_type(commit_text: &str) -> Option<String> {
if commit_text.contains("-----BEGIN SSH SIGNATURE-----") {
return Some("ssh".to_string());
Expand Down Expand Up @@ -524,14 +572,14 @@ mod tests {
"a\x1fG\x1fAlice\x1fABC123\x1fABC123",
])
.with_key_type("a", "gpg")
.with_gpg_program("/usr/local/bin/gpg");
.with_gpg_program("gpg");
let results = verify_commit_signatures("/repo", &["a".to_string()], true, &runner)
.expect("verification should complete");

assert_eq!(results[0].status, SignatureStatus::Verified);
assert_eq!(
runner.fetched_keys.borrow().as_slice(),
&["/usr/local/bin/gpg ABC123".to_string()]
&["gpg ABC123".to_string()]
);
assert_eq!(
runner.verified_hashes.borrow().as_slice(),
Expand Down Expand Up @@ -599,6 +647,48 @@ mod tests {

assert_eq!(error, "fatal: bad revision");
}

#[test]
fn validate_gpg_program_rejects_empty_string() {
assert_eq!(validate_gpg_program_from_repo(""), None);
}

#[test]
fn validate_gpg_program_rejects_relative_paths() {
assert_eq!(validate_gpg_program_from_repo("./malicious/gpg"), None);
}

#[test]
fn validate_gpg_program_rejects_relative_paths_with_backslash() {
assert_eq!(validate_gpg_program_from_repo(".\\malicious\\gpg"), None);
}

#[test]
fn validate_gpg_program_rejects_nonexistent_absolute_paths() {
assert_eq!(
validate_gpg_program_from_repo("/nonexistent/path/to/gpg"),
None
);
}

#[test]
fn validate_gpg_program_rejects_nonexistent_program_names() {
assert_eq!(
validate_gpg_program_from_repo("nonexistent_gpg_program_12345"),
None
);
}

#[test]
fn validate_gpg_program_accepts_existing_absolute_file() {
let executable = std::env::current_exe().expect("current test executable");
let executable = executable.to_str().expect("UTF-8 executable path");

assert_eq!(
validate_gpg_program_from_repo(executable),
Some(executable.to_string())
);
}
}

#[tauri::command]
Expand Down