// Copyright 2023 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #![expect(missing_docs)] use std::ffi::OsString; use std::fmt::Debug; use std::io; use std::io::Write as _; use std::process::Command; use std::process::ExitStatus; use std::process::Stdio; use thiserror::Error; use crate::config::ConfigGetError; use crate::settings::UserSettings; use crate::signing::SigStatus; use crate::signing::SignError; use crate::signing::SigningBackend; use crate::signing::Verification; // Search for one of the: // [GNUPG:] GOODSIG // [GNUPG:] EXPKEYSIG // [GNUPG:] NO_PUBKEY // [GNUPG:] BADSIG // in the output from --status-fd=1 // Assume signature is invalid if none of the above was found fn parse_gpg_verify_output( output: &[u8], allow_expired_keys: bool, ) -> Result { output .split(|&b| b == b'\n') .filter_map(|line| line.strip_prefix(b"[GNUPG:] ")) .find_map(|line| { let mut parts = line.splitn(3, |&b| b == b' ').fuse(); let status = match parts.next()? { b"GOODSIG" => SigStatus::Good, b"EXPKEYSIG" => { if allow_expired_keys { SigStatus::Good } else { SigStatus::Bad } } b"NO_PUBKEY" => SigStatus::Unknown, b"BADSIG" => SigStatus::Bad, b"ERROR" => match parts.next()? { b"verify.findkey" => return Some(Verification::unknown()), _ => return None, }, _ => return None, }; let key = parts .next() .and_then(|bs| str::from_utf8(bs).ok()) .map(|value| value.trim().to_owned()); let display = parts .next() .and_then(|bs| str::from_utf8(bs).ok()) .map(|value| value.trim().to_owned()); Some(Verification::new(status, key, display)) }) .ok_or(SignError::InvalidSignatureFormat) } fn run_sign_command(command: &mut Command, input: &[u8]) -> Result, GpgError> { tracing::info!(?command, "running GPG signing command"); let process = command.stderr(Stdio::piped()).spawn()?; let write_result = process.stdin.as_ref().unwrap().write_all(input); let output = process.wait_with_output()?; tracing::info!(?command, ?output.status, "GPG signing command exited"); if output.status.success() { write_result?; Ok(output.stdout) } else { Err(GpgError::Command { exit_status: output.status, stderr: String::from_utf8_lossy(&output.stderr).trim_end().into(), }) } } fn run_verify_command(command: &mut Command, input: &[u8]) -> Result, GpgError> { tracing::info!(?command, "running GPG signing command"); let process = command.stderr(Stdio::null()).spawn()?; let write_result = process.stdin.as_ref().unwrap().write_all(input); let output = process.wait_with_output()?; tracing::info!(?command, ?output.status, "GPG signing command exited"); match write_result { Ok(()) => Ok(output.stdout), // If the signature format is invalid, gpg will terminate early. Writing // more input data will fail in that case. Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(vec![]), Err(err) => Err(err.into()), } } #[derive(Debug)] pub struct GpgBackend { program: OsString, allow_expired_keys: bool, extra_args: Vec, default_key: String, } #[derive(Debug, Error)] pub enum GpgError { #[error("GPG failed with {exit_status}:\n{stderr}")] Command { exit_status: ExitStatus, stderr: String, }, #[error("Failed to run GPG")] Io(#[from] std::io::Error), } impl From for SignError { fn from(e: GpgError) -> Self { Self::Backend(Box::new(e)) } } impl GpgBackend { pub fn new(program: OsString, allow_expired_keys: bool, default_key: String) -> Self { Self { program, allow_expired_keys, extra_args: vec![], default_key, } } /// Primarily intended for testing pub fn with_extra_args(mut self, args: &[OsString]) -> Self { self.extra_args.extend_from_slice(args); self } pub fn from_settings(settings: &UserSettings) -> Result { let program = settings.get_string("signing.backends.gpg.program")?; let allow_expired_keys = settings.get_bool("signing.backends.gpg.allow-expired-keys")?; let default_key = settings.user_email().to_owned(); Ok(Self::new(program.into(), allow_expired_keys, default_key)) } fn create_command(&self) -> Command { let mut command = Command::new(&self.program); // Hide console window on Windows (https://stackoverflow.com/a/60958956) #[cfg(windows)] { use std::os::windows::process::CommandExt as _; const CREATE_NO_WINDOW: u32 = 0x08000000; command.creation_flags(CREATE_NO_WINDOW); } command .stdin(Stdio::piped()) .stdout(Stdio::piped()) .args(&self.extra_args); command } } impl SigningBackend for GpgBackend { fn name(&self) -> &str { "gpg" } fn can_read(&self, signature: &[u8]) -> bool { signature.starts_with(b"-----BEGIN PGP SIGNATURE-----") } fn sign(&self, data: &[u8], key: Option<&str>) -> Result, SignError> { let key = key.unwrap_or(&self.default_key); Ok(run_sign_command( self.create_command().args(["-abu", key]), data, )?) } fn verify(&self, data: &[u8], signature: &[u8]) -> Result { let mut signature_file = tempfile::Builder::new() .prefix(".jj-gpg-sig-tmp-") .tempfile() .map_err(GpgError::Io)?; signature_file.write_all(signature).map_err(GpgError::Io)?; signature_file.flush().map_err(GpgError::Io)?; let sig_path = signature_file.into_temp_path(); let output = run_verify_command( self.create_command() .args(["--keyid-format=long", "--status-fd=1", "--verify"]) .arg(&sig_path) .arg("-"), data, )?; parse_gpg_verify_output(&output, self.allow_expired_keys) } } #[derive(Debug)] pub struct GpgsmBackend { program: OsString, allow_expired_keys: bool, extra_args: Vec, default_key: String, } impl GpgsmBackend { pub fn new(program: OsString, allow_expired_keys: bool, default_key: String) -> Self { Self { program, allow_expired_keys, extra_args: vec![], default_key, } } /// Primarily intended for testing pub fn with_extra_args(mut self, args: &[OsString]) -> Self { self.extra_args.extend_from_slice(args); self } pub fn from_settings(settings: &UserSettings) -> Result { let program = settings.get_string("signing.backends.gpgsm.program")?; let allow_expired_keys = settings.get_bool("signing.backends.gpgsm.allow-expired-keys")?; let default_key = settings.user_email().to_owned(); Ok(Self::new(program.into(), allow_expired_keys, default_key)) } fn create_command(&self) -> Command { let mut command = Command::new(&self.program); // Hide console window on Windows (https://stackoverflow.com/a/60958956) #[cfg(windows)] { use std::os::windows::process::CommandExt as _; const CREATE_NO_WINDOW: u32 = 0x08000000; command.creation_flags(CREATE_NO_WINDOW); } command .stdin(Stdio::piped()) .stdout(Stdio::piped()) .args(&self.extra_args); command } } impl SigningBackend for GpgsmBackend { fn name(&self) -> &str { "gpgsm" } fn can_read(&self, signature: &[u8]) -> bool { signature.starts_with(b"-----BEGIN SIGNED MESSAGE-----") } fn sign(&self, data: &[u8], key: Option<&str>) -> Result, SignError> { let key = key.unwrap_or(&self.default_key); Ok(run_sign_command( self.create_command().args(["-abu", key]), data, )?) } fn verify(&self, data: &[u8], signature: &[u8]) -> Result { let mut signature_file = tempfile::Builder::new() .prefix(".jj-gpgsm-sig-tmp-") .tempfile() .map_err(GpgError::Io)?; signature_file.write_all(signature).map_err(GpgError::Io)?; signature_file.flush().map_err(GpgError::Io)?; let sig_path = signature_file.into_temp_path(); let output = run_verify_command( self.create_command() .args(["--status-fd=1", "--verify"]) .arg(&sig_path) .arg("-"), data, )?; parse_gpg_verify_output(&output, self.allow_expired_keys) } } #[cfg(test)] mod tests { use super::*; #[test] fn gpg_verify_invalid_signature_format() { use assert_matches::assert_matches; assert_matches!( parse_gpg_verify_output(b"", true), Err(SignError::InvalidSignatureFormat) ); } #[test] fn gpg_verify_bad_signature() { assert_eq!( parse_gpg_verify_output(b"[GNUPG:] BADSIG 123 456", true).unwrap(), Verification::new(SigStatus::Bad, Some("123".into()), Some("456".into())) ); } #[test] fn gpg_verify_unknown_signature() { assert_eq!( parse_gpg_verify_output(b"[GNUPG:] NO_PUBKEY 123", true).unwrap(), Verification::new(SigStatus::Unknown, Some("123".into()), None) ); } #[test] fn gpg_verify_good_signature() { assert_eq!( parse_gpg_verify_output(b"[GNUPG:] GOODSIG 123 456", true).unwrap(), Verification::new(SigStatus::Good, Some("123".into()), Some("456".into())) ); } #[test] fn gpg_verify_expired_signature() { assert_eq!( parse_gpg_verify_output(b"[GNUPG:] EXPKEYSIG 123 456", true).unwrap(), Verification::new(SigStatus::Good, Some("123".into()), Some("456".into())) ); assert_eq!( parse_gpg_verify_output(b"[GNUPG:] EXPKEYSIG 123 456", false).unwrap(), Verification::new(SigStatus::Bad, Some("123".into()), Some("456".into())) ); } #[test] fn gpgsm_verify_unknown_signature() { assert_eq!( parse_gpg_verify_output(b"[GNUPG:] ERROR verify.findkey 50331657", true).unwrap(), Verification::unknown(), ); } #[test] fn gpgsm_verify_invalid_signature_format() { use assert_matches::assert_matches; assert_matches!( parse_gpg_verify_output(b"[GNUPG:] ERROR verify.leave 150995087", true), Err(SignError::InvalidSignatureFormat) ); } }