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