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