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