1#![expect(missing_docs)]
16
17use std::ffi::OsString;
18use std::fmt::Debug;
19use std::io;
20use std::io::Write as _;
21use std::process;
22use std::process::Command;
23use std::process::ExitStatus;
24use std::process::Stdio;
25
26use thiserror::Error;
27
28use crate::config::ConfigGetError;
29use crate::settings::UserSettings;
30use crate::signing::SigStatus;
31use crate::signing::SignError;
32use crate::signing::SigningBackend;
33use crate::signing::Verification;
34
35fn parse_gpg_verify_output(
47 output: &[u8],
48 allow_expired_keys: bool,
49) -> Option<Result<Verification, SignError>> {
50 let status_lines = || {
51 output.split(|&b| b == b'\n').filter_map(|line| {
52 let line = line.strip_prefix(b"[GNUPG:] ")?;
53 let mut parts = line.splitn(3, |&b| b == b' ');
54 Some((parts.next()?, parts))
55 })
56 };
57 let maybe_verification = status_lines().find_map(|(name, mut args)| {
58 let status = match name {
59 b"GOODSIG" => SigStatus::Good,
60 b"EXPKEYSIG" => {
61 if allow_expired_keys {
62 SigStatus::Good
63 } else {
64 SigStatus::Bad
65 }
66 }
67 b"NO_PUBKEY" => SigStatus::Unknown,
68 b"BADSIG" => SigStatus::Bad,
69 b"ERROR" => match args.next()? {
70 b"verify.findkey" => return Some(Verification::unknown()),
71 _ => return None,
72 },
73 _ => return None,
74 };
75 let mut args = args.fuse();
76 let key = args
77 .next()
78 .and_then(|bs| str::from_utf8(bs).ok())
79 .map(|value| value.trim().to_owned());
80 let display = args
81 .next()
82 .and_then(|bs| str::from_utf8(bs).ok())
83 .map(|value| value.trim().to_owned());
84 Some(Verification::new(status, key, display))
85 });
86 if let Some(verification) = maybe_verification {
87 Some(Ok(verification))
88 } else if status_lines().any(|(name, _)| name != b"FAILURE") {
89 Some(Err(SignError::InvalidSignatureFormat))
90 } else {
91 None
92 }
93}
94
95fn make_command_error(output: &process::Output) -> GpgError {
96 GpgError::Command {
97 exit_status: output.status,
98 stderr: String::from_utf8_lossy(&output.stderr).trim_end().into(),
99 }
100}
101
102fn run_sign_command(command: &mut Command, input: &[u8]) -> Result<Vec<u8>, GpgError> {
103 tracing::info!(?command, "running GPG signing command");
104 let process = command.stderr(Stdio::piped()).spawn()?;
105 let write_result = process.stdin.as_ref().unwrap().write_all(input);
106 let output = process.wait_with_output()?;
107 tracing::info!(?command, ?output.status, "GPG signing command exited");
108 if output.status.success() {
109 write_result?;
110 Ok(output.stdout)
111 } else {
112 Err(make_command_error(&output))
113 }
114}
115
116fn run_verify_command(command: &mut Command, input: &[u8]) -> Result<process::Output, GpgError> {
117 tracing::info!(?command, "running GPG signing command");
118 let process = command.stderr(Stdio::piped()).spawn()?;
119 let write_result = process.stdin.as_ref().unwrap().write_all(input);
120 let output = process.wait_with_output()?;
121 tracing::info!(?command, ?output.status, "GPG signing command exited");
122 match write_result {
123 Ok(()) => Ok(output),
124 Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(output),
127 Err(err) => Err(err.into()),
128 }
129}
130
131fn write_temp_file(prefix: &str, content: &[u8]) -> io::Result<tempfile::TempPath> {
132 let mut file = tempfile::Builder::new().prefix(prefix).tempfile()?;
133 file.write_all(content)?;
134 file.flush()?;
135 Ok(file.into_temp_path())
136}
137
138#[derive(Debug)]
139pub struct GpgBackend {
140 program: OsString,
141 allow_expired_keys: bool,
142 extra_args: Vec<OsString>,
143 default_key: String,
144}
145
146#[derive(Debug, Error)]
147pub enum GpgError {
148 #[error("GPG failed with {exit_status}:\n{stderr}")]
149 Command {
150 exit_status: ExitStatus,
151 stderr: String,
152 },
153 #[error("Failed to run GPG")]
154 Io(#[from] std::io::Error),
155}
156
157impl From<GpgError> for SignError {
158 fn from(e: GpgError) -> Self {
159 Self::Backend(Box::new(e))
160 }
161}
162
163impl GpgBackend {
164 pub fn new(program: OsString, allow_expired_keys: bool, default_key: String) -> Self {
165 Self {
166 program,
167 allow_expired_keys,
168 extra_args: vec![],
169 default_key,
170 }
171 }
172
173 pub fn with_extra_args(mut self, args: &[OsString]) -> Self {
175 self.extra_args.extend_from_slice(args);
176 self
177 }
178
179 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
180 let program = settings.get_string("signing.backends.gpg.program")?;
181 let allow_expired_keys = settings.get_bool("signing.backends.gpg.allow-expired-keys")?;
182 let default_key = settings.user_email().to_owned();
183 Ok(Self::new(program.into(), allow_expired_keys, default_key))
184 }
185
186 fn create_command(&self) -> Command {
187 let mut command = Command::new(&self.program);
188 #[cfg(windows)]
190 {
191 use std::os::windows::process::CommandExt as _;
192 const CREATE_NO_WINDOW: u32 = 0x08000000;
193 command.creation_flags(CREATE_NO_WINDOW);
194 }
195
196 command
197 .stdin(Stdio::piped())
198 .stdout(Stdio::piped())
199 .args(&self.extra_args);
200 command
201 }
202}
203
204impl SigningBackend for GpgBackend {
205 fn name(&self) -> &'static str {
206 "gpg"
207 }
208
209 fn can_read(&self, signature: &[u8]) -> bool {
210 signature.starts_with(b"-----BEGIN PGP SIGNATURE-----")
211 }
212
213 fn sign(&self, data: &[u8], key: Option<&str>) -> Result<Vec<u8>, SignError> {
214 let key = key.unwrap_or(&self.default_key);
215 Ok(run_sign_command(
216 self.create_command().args(["-abu", key]),
217 data,
218 )?)
219 }
220
221 fn verify(&self, data: &[u8], signature: &[u8]) -> Result<Verification, SignError> {
222 let sig_path = write_temp_file(".jj-gpg-sig-tmp-", signature).map_err(GpgError::Io)?;
223
224 let output = run_verify_command(
225 self.create_command()
226 .args(["--keyid-format=long", "--status-fd=1", "--verify"])
227 .arg(&sig_path)
228 .arg("-"),
229 data,
230 )?;
231
232 parse_gpg_verify_output(&output.stdout, self.allow_expired_keys)
233 .unwrap_or_else(|| Err(make_command_error(&output).into()))
234 }
235}
236
237#[derive(Debug)]
238pub struct GpgsmBackend {
239 program: OsString,
240 allow_expired_keys: bool,
241 extra_args: Vec<OsString>,
242 default_key: String,
243}
244
245impl GpgsmBackend {
246 pub fn new(program: OsString, allow_expired_keys: bool, default_key: String) -> Self {
247 Self {
248 program,
249 allow_expired_keys,
250 extra_args: vec![],
251 default_key,
252 }
253 }
254
255 pub fn with_extra_args(mut self, args: &[OsString]) -> Self {
257 self.extra_args.extend_from_slice(args);
258 self
259 }
260
261 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
262 let program = settings.get_string("signing.backends.gpgsm.program")?;
263 let allow_expired_keys = settings.get_bool("signing.backends.gpgsm.allow-expired-keys")?;
264 let default_key = settings.user_email().to_owned();
265 Ok(Self::new(program.into(), allow_expired_keys, default_key))
266 }
267
268 fn create_command(&self) -> Command {
269 let mut command = Command::new(&self.program);
270 #[cfg(windows)]
272 {
273 use std::os::windows::process::CommandExt as _;
274 const CREATE_NO_WINDOW: u32 = 0x08000000;
275 command.creation_flags(CREATE_NO_WINDOW);
276 }
277
278 command
279 .stdin(Stdio::piped())
280 .stdout(Stdio::piped())
281 .args(&self.extra_args);
282 command
283 }
284}
285
286impl SigningBackend for GpgsmBackend {
287 fn name(&self) -> &'static str {
288 "gpgsm"
289 }
290
291 fn can_read(&self, signature: &[u8]) -> bool {
292 signature.starts_with(b"-----BEGIN SIGNED MESSAGE-----")
293 }
294
295 fn sign(&self, data: &[u8], key: Option<&str>) -> Result<Vec<u8>, SignError> {
296 let key = key.unwrap_or(&self.default_key);
297 Ok(run_sign_command(
298 self.create_command().args(["-abu", key]),
299 data,
300 )?)
301 }
302
303 fn verify(&self, data: &[u8], signature: &[u8]) -> Result<Verification, SignError> {
304 let data_path = write_temp_file(".jj-gpgsm-data-tmp-", data).map_err(GpgError::Io)?;
305 let sig_path = write_temp_file(".jj-gpgsm-sig-tmp-", signature).map_err(GpgError::Io)?;
306
307 let output = run_verify_command(
309 self.create_command()
310 .args(["--status-fd=1", "--verify"])
311 .arg(&sig_path)
312 .arg(&data_path),
313 b"",
314 )?;
315
316 parse_gpg_verify_output(&output.stdout, self.allow_expired_keys)
317 .unwrap_or_else(|| Err(make_command_error(&output).into()))
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use assert_matches::assert_matches;
324
325 use super::*;
326
327 #[test]
328 fn gpg_verify_invalid_signature_format() {
329 assert_matches!(
330 parse_gpg_verify_output(
331 b"[GNUPG:] NODATA 4\n[GNUPG:] FAILURE gpg-exit 33554433\n",
332 true
333 ),
334 Some(Err(SignError::InvalidSignatureFormat))
335 );
336 }
337
338 #[test]
339 fn gpg_verify_bad_signature() {
340 assert_eq!(
341 parse_gpg_verify_output(b"[GNUPG:] BADSIG 123 456", true)
342 .unwrap()
343 .unwrap(),
344 Verification::new(SigStatus::Bad, Some("123".into()), Some("456".into()))
345 );
346 }
347
348 #[test]
349 fn gpg_verify_unknown_signature() {
350 assert_eq!(
351 parse_gpg_verify_output(b"[GNUPG:] NO_PUBKEY 123", true)
352 .unwrap()
353 .unwrap(),
354 Verification::new(SigStatus::Unknown, Some("123".into()), None)
355 );
356 }
357
358 #[test]
359 fn gpg_verify_good_signature() {
360 assert_eq!(
361 parse_gpg_verify_output(b"[GNUPG:] GOODSIG 123 456", true)
362 .unwrap()
363 .unwrap(),
364 Verification::new(SigStatus::Good, Some("123".into()), Some("456".into()))
365 );
366 }
367
368 #[test]
369 fn gpg_verify_expired_signature() {
370 assert_eq!(
371 parse_gpg_verify_output(b"[GNUPG:] EXPKEYSIG 123 456", true)
372 .unwrap()
373 .unwrap(),
374 Verification::new(SigStatus::Good, Some("123".into()), Some("456".into()))
375 );
376
377 assert_eq!(
378 parse_gpg_verify_output(b"[GNUPG:] EXPKEYSIG 123 456", false)
379 .unwrap()
380 .unwrap(),
381 Verification::new(SigStatus::Bad, Some("123".into()), Some("456".into()))
382 );
383 }
384
385 #[test]
386 fn gpg_verify_unknown_error() {
387 assert_matches!(parse_gpg_verify_output(b"", true), None);
388 assert_matches!(
389 parse_gpg_verify_output(b"[GNUPG:] FAILURE gpg-exit 33554433\n", true),
390 None
391 );
392 assert_matches!(
393 parse_gpg_verify_output(b"[GNUPG:] FAILURE gpgsm-exit 50331649\n", true),
394 None
395 );
396 }
397
398 #[test]
399 fn gpgsm_verify_unknown_signature() {
400 assert_eq!(
401 parse_gpg_verify_output(b"[GNUPG:] ERROR verify.findkey 50331657", true)
402 .unwrap()
403 .unwrap(),
404 Verification::unknown(),
405 );
406 }
407
408 #[test]
409 fn gpgsm_verify_invalid_signature_format() {
410 assert_matches!(
411 parse_gpg_verify_output(b"[GNUPG:] ERROR verify.leave 150995087", true),
412 Some(Err(SignError::InvalidSignatureFormat))
413 );
414 }
415}