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