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#![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
35// Search for one of the:
36//  [GNUPG:] GOODSIG <long keyid> <primary uid..>
37//  [GNUPG:] EXPKEYSIG <long keyid> <primary uid..>
38//  [GNUPG:] NO_PUBKEY <long keyid>
39//  [GNUPG:] BADSIG <long keyid> <primary uid..>
40// in the output from --status-fd=1
41// Assume signature is invalid if none of the above was found
42fn 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        // If the signature format is invalid, gpg will terminate early. Writing
107        // more input data will fail in that case.
108        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    /// Primarily intended for testing
149    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        // Hide console window on Windows (https://stackoverflow.com/a/60958956)
164        #[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    /// Primarily intended for testing
237    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        // Hide console window on Windows (https://stackoverflow.com/a/60958956)
252        #[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}