Skip to main content

jj_lib/
gpg_signing.rs

1// Copyright 2023 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![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
35/// Search for one of these in the output from `--status-fd=1`.
36///
37/// - `[GNUPG:] GOODSIG <long keyid> <primary uid..>`
38/// - `[GNUPG:] EXPKEYSIG <long keyid> <primary uid..>`
39/// - `[GNUPG:] NO_PUBKEY <long keyid>`
40/// - `[GNUPG:] BADSIG <long keyid> <primary uid..>`
41///
42/// Assume signature is invalid if none of the above was found, and if there are
43/// at least one line other than general program failures `[GNUPG:] FAILURE`.
44///
45/// https://github.com/gpg/gnupg/blob/gnupg-2.5.18/doc/DETAILS#format-of-the-status-fd-output
46fn 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        // If the signature format is invalid, gpg will terminate early. Writing
125        // more input data will fail in that case.
126        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    /// Primarily intended for testing
174    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        // Hide console window on Windows (https://stackoverflow.com/a/60958956)
189        #[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    /// Primarily intended for testing
256    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        // Hide console window on Windows (https://stackoverflow.com/a/60958956)
271        #[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        // gpgsm 2.5.x doesn't parse "-" as stdin
308        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}