// Copyright 2023 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. #![allow(missing_docs)] use std::ffi::OsString;use std::fmt::Debug;use std::io;use std::io::Write as _;use std::process::Command;use std::process::ExitStatus;use std::process::Stdio;use std::str; use thiserror::Error; use crate::config::ConfigGetError;use crate::settings::UserSettings;use crate::signing::SigStatus;use crate::signing::SignError;use crate::signing::SigningBackend;use crate::signing::Verification; // Search for one of the:// [GNUPG:] GOODSIG <long keyid> <primary uid..>// [GNUPG:] EXPKEYSIG <long keyid> <primary uid..>// [GNUPG:] NO_PUBKEY <long keyid>// [GNUPG:] BADSIG <long keyid> <primary uid..>// in the output from --status-fd=1// Assume signature is invalid if none of the above was foundfn parse_gpg_verify_output( output: &[u8], allow_expired_keys: bool,) -> Result<Verification, SignError> { output .split(|&b| b == b'\n') .filter_map(|line| line.strip_prefix(b"[GNUPG:] ")) .find_map(|line| { let mut parts = line.splitn(3, |&b| b == b' ').fuse(); let status = match parts.next()? { b"GOODSIG" => SigStatus::Good, b"EXPKEYSIG" => { if allow_expired_keys { SigStatus::Good } else { SigStatus::Bad } } b"NO_PUBKEY" => SigStatus::Unknown, b"BADSIG" => SigStatus::Bad, b"ERROR" => match parts.next()? { b"verify.findkey" => return Some(Verification::unknown()), _ => return None, }, _ => return None, }; let key = parts .next() .and_then(|bs| str::from_utf8(bs).ok()) .map(|value| value.trim().to_owned()); let display = parts .next() .and_then(|bs| str::from_utf8(bs).ok()) .map(|value| value.trim().to_owned()); Some(Verification::new(status, key, display)) }) .ok_or(SignError::InvalidSignatureFormat)} fn run_sign_command(command: &mut Command, input: &[u8]) -> Result<Vec<u8>, GpgError> { tracing::info!(?command, "running GPG signing command"); let process = command.stderr(Stdio::piped()).spawn()?; let write_result = process.stdin.as_ref().unwrap().write_all(input); let output = process.wait_with_output()?; tracing::info!(?command, ?output.status, "GPG signing command exited"); if output.status.success() { write_result?; Ok(output.stdout) } else { Err(GpgError::Command { exit_status: output.status, stderr: String::from_utf8_lossy(&output.stderr).trim_end().into(), }) }} fn run_verify_command(command: &mut Command, input: &[u8]) -> Result<Vec<u8>, GpgError> { tracing::info!(?command, "running GPG signing command"); let process = command.stderr(Stdio::null()).spawn()?; let write_result = process.stdin.as_ref().unwrap().write_all(input); let output = process.wait_with_output()?; tracing::info!(?command, ?output.status, "GPG signing command exited"); match write_result { Ok(()) => Ok(output.stdout), // If the signature format is invalid, gpg will terminate early. Writing // more input data will fail in that case. Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(vec![]), Err(err) => Err(err.into()), }} #[derive(Debug)]pub struct GpgBackend { program: OsString, allow_expired_keys: bool, extra_args: Vec<OsString>, default_key: String,} #[derive(Debug, Error)]pub enum GpgError { #[error("GPG failed with {exit_status}:\n{stderr}")] Command { exit_status: ExitStatus, stderr: String, }, #[error("Failed to run GPG")] Io(#[from] std::io::Error),} impl From<GpgError> for SignError { fn from(e: GpgError) -> Self { SignError::Backend(Box::new(e)) }} impl GpgBackend { pub fn new(program: OsString, allow_expired_keys: bool, default_key: String) -> Self { Self { program, allow_expired_keys, extra_args: vec![], default_key, } } /// Primarily intended for testing pub fn with_extra_args(mut self, args: &[OsString]) -> Self { self.extra_args.extend_from_slice(args); self } pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> { let program = settings.get_string("signing.backends.gpg.program")?; let allow_expired_keys = settings.get_bool("signing.backends.gpg.allow-expired-keys")?; let default_key = settings.user_email().to_owned(); Ok(Self::new(program.into(), allow_expired_keys, default_key)) } fn create_command(&self) -> Command { let mut command = Command::new(&self.program); // Hide console window on Windows (https://stackoverflow.com/a/60958956) #[cfg(windows)] { use std::os::windows::process::CommandExt; const CREATE_NO_WINDOW: u32 = 0x08000000; command.creation_flags(CREATE_NO_WINDOW); } command .stdin(Stdio::piped()) .stdout(Stdio::piped()) .args(&self.extra_args); command }} impl SigningBackend for GpgBackend { fn name(&self) -> &str { "gpg" } fn can_read(&self, signature: &[u8]) -> bool { signature.starts_with(b"-----BEGIN PGP SIGNATURE-----") } fn sign(&self, data: &[u8], key: Option<&str>) -> Result<Vec<u8>, SignError> { let key = key.unwrap_or(&self.default_key); Ok(run_sign_command( self.create_command().args(["-abu", key]), data, )?) } fn verify(&self, data: &[u8], signature: &[u8]) -> Result<Verification, SignError> { let mut signature_file = tempfile::Builder::new() .prefix(".jj-gpg-sig-tmp-") .tempfile() .map_err(GpgError::Io)?; signature_file.write_all(signature).map_err(GpgError::Io)?; signature_file.flush().map_err(GpgError::Io)?; let sig_path = signature_file.into_temp_path(); let output = run_verify_command( self.create_command() .args(["--keyid-format=long", "--status-fd=1", "--verify"]) .arg(&sig_path) .arg("-"), data, )?; parse_gpg_verify_output(&output, self.allow_expired_keys) }} #[derive(Debug)]pub struct GpgsmBackend { program: OsString, allow_expired_keys: bool, extra_args: Vec<OsString>, default_key: String,} impl GpgsmBackend { pub fn new(program: OsString, allow_expired_keys: bool, default_key: String) -> Self { Self { program, allow_expired_keys, extra_args: vec![], default_key, } } /// Primarily intended for testing pub fn with_extra_args(mut self, args: &[OsString]) -> Self { self.extra_args.extend_from_slice(args); self } pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> { let program = settings.get_string("signing.backends.gpgsm.program")?; let allow_expired_keys = settings.get_bool("signing.backends.gpgsm.allow-expired-keys")?; let default_key = settings.user_email().to_owned(); Ok(Self::new(program.into(), allow_expired_keys, default_key)) } fn create_command(&self) -> Command { let mut command = Command::new(&self.program); // Hide console window on Windows (https://stackoverflow.com/a/60958956) #[cfg(windows)] { use std::os::windows::process::CommandExt; const CREATE_NO_WINDOW: u32 = 0x08000000; command.creation_flags(CREATE_NO_WINDOW); } command .stdin(Stdio::piped()) .stdout(Stdio::piped()) .args(&self.extra_args); command }} impl SigningBackend for GpgsmBackend { fn name(&self) -> &str { "gpgsm" } fn can_read(&self, signature: &[u8]) -> bool { signature.starts_with(b"-----BEGIN SIGNED MESSAGE-----") } fn sign(&self, data: &[u8], key: Option<&str>) -> Result<Vec<u8>, SignError> { let key = key.unwrap_or(&self.default_key); Ok(run_sign_command( self.create_command().args(["-abu", key]), data, )?) } fn verify(&self, data: &[u8], signature: &[u8]) -> Result<Verification, SignError> { let mut signature_file = tempfile::Builder::new() .prefix(".jj-gpgsm-sig-tmp-") .tempfile() .map_err(GpgError::Io)?; signature_file.write_all(signature).map_err(GpgError::Io)?; signature_file.flush().map_err(GpgError::Io)?; let sig_path = signature_file.into_temp_path(); let output = run_verify_command( self.create_command() .args(["--status-fd=1", "--verify"]) .arg(&sig_path) .arg("-"), data, )?; parse_gpg_verify_output(&output, self.allow_expired_keys) }} #[cfg(test)]mod tests { use super::*; #[test] fn gpg_verify_invalid_signature_format() { use assert_matches::assert_matches; assert_matches!( parse_gpg_verify_output(b"", true), Err(SignError::InvalidSignatureFormat) ); } #[test] fn gpg_verify_bad_signature() { assert_eq!( parse_gpg_verify_output(b"[GNUPG:] BADSIG 123 456", true).unwrap(), Verification::new(SigStatus::Bad, Some("123".into()), Some("456".into())) ); } #[test] fn gpg_verify_unknown_signature() { assert_eq!( parse_gpg_verify_output(b"[GNUPG:] NO_PUBKEY 123", true).unwrap(), Verification::new(SigStatus::Unknown, Some("123".into()), None) ); } #[test] fn gpg_verify_good_signature() { assert_eq!( parse_gpg_verify_output(b"[GNUPG:] GOODSIG 123 456", true).unwrap(), Verification::new(SigStatus::Good, Some("123".into()), Some("456".into())) ); } #[test] fn gpg_verify_expired_signature() { assert_eq!( parse_gpg_verify_output(b"[GNUPG:] EXPKEYSIG 123 456", true).unwrap(), Verification::new(SigStatus::Good, Some("123".into()), Some("456".into())) ); assert_eq!( parse_gpg_verify_output(b"[GNUPG:] EXPKEYSIG 123 456", false).unwrap(), Verification::new(SigStatus::Bad, Some("123".into()), Some("456".into())) ); } #[test] fn gpgsm_verify_unknown_signature() { assert_eq!( parse_gpg_verify_output(b"[GNUPG:] ERROR verify.findkey 50331657", true).unwrap(), Verification::unknown(), ); } #[test] fn gpgsm_verify_invalid_signature_format() { use assert_matches::assert_matches; assert_matches!( parse_gpg_verify_output(b"[GNUPG:] ERROR verify.leave 150995087", true), Err(SignError::InvalidSignatureFormat) ); }}
// Copyright 2023 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(missing_docs)]
use std::ffi::OsString;
use std::fmt::Debug;
use std::io;
use std::io::Write as _;
use std::process::Command;
use std::process::ExitStatus;
use std::process::Stdio;
use std::str;
use thiserror::Error;
use crate::config::ConfigGetError;
use crate::settings::UserSettings;
use crate::signing::SigStatus;
use crate::signing::SignError;
use crate::signing::SigningBackend;
use crate::signing::Verification;
// Search for one of the:
// [GNUPG:] GOODSIG <long keyid> <primary uid..>
// [GNUPG:] EXPKEYSIG <long keyid> <primary uid..>
// [GNUPG:] NO_PUBKEY <long keyid>
// [GNUPG:] BADSIG <long keyid> <primary uid..>
// in the output from --status-fd=1
// Assume signature is invalid if none of the above was found
fn parse_gpg_verify_output(
output: &[u8],
allow_expired_keys: bool,
) -> Result<Verification, SignError> {
output
.split(|&b| b == b'\n')
.filter_map(|line| line.strip_prefix(b"[GNUPG:] "))
.find_map(|line| {
let mut parts = line.splitn(3, |&b| b == b' ').fuse();
let status = match parts.next()? {
b"GOODSIG" => SigStatus::Good,
b"EXPKEYSIG" => {
if allow_expired_keys {
SigStatus::Good
} else {
SigStatus::Bad
}
}
b"NO_PUBKEY" => SigStatus::Unknown,
b"BADSIG" => SigStatus::Bad,
b"ERROR" => match parts.next()? {
b"verify.findkey" => return Some(Verification::unknown()),
_ => return None,
},
_ => return None,
};
let key = parts
.next()
.and_then(|bs| str::from_utf8(bs).ok())
.map(|value| value.trim().to_owned());
let display = parts
.next()
.and_then(|bs| str::from_utf8(bs).ok())
.map(|value| value.trim().to_owned());
Some(Verification::new(status, key, display))
})
.ok_or(SignError::InvalidSignatureFormat)
}
fn run_sign_command(command: &mut Command, input: &[u8]) -> Result<Vec<u8>, GpgError> {
tracing::info!(?command, "running GPG signing command");
let process = command.stderr(Stdio::piped()).spawn()?;
let write_result = process.stdin.as_ref().unwrap().write_all(input);
let output = process.wait_with_output()?;
tracing::info!(?command, ?output.status, "GPG signing command exited");
if output.status.success() {
write_result?;
Ok(output.stdout)
} else {
Err(GpgError::Command {
exit_status: output.status,
stderr: String::from_utf8_lossy(&output.stderr).trim_end().into(),
})
}
}
fn run_verify_command(command: &mut Command, input: &[u8]) -> Result<Vec<u8>, GpgError> {
tracing::info!(?command, "running GPG signing command");
let process = command.stderr(Stdio::null()).spawn()?;
let write_result = process.stdin.as_ref().unwrap().write_all(input);
let output = process.wait_with_output()?;
tracing::info!(?command, ?output.status, "GPG signing command exited");
match write_result {
Ok(()) => Ok(output.stdout),
// If the signature format is invalid, gpg will terminate early. Writing
// more input data will fail in that case.
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(vec![]),
Err(err) => Err(err.into()),
}
}
#[derive(Debug)]
pub struct GpgBackend {
program: OsString,
allow_expired_keys: bool,
extra_args: Vec<OsString>,
default_key: String,
}
#[derive(Debug, Error)]
pub enum GpgError {
#[error("GPG failed with {exit_status}:\n{stderr}")]
Command {
exit_status: ExitStatus,
stderr: String,
},
#[error("Failed to run GPG")]
Io(#[from] std::io::Error),
}
impl From<GpgError> for SignError {
fn from(e: GpgError) -> Self {
SignError::Backend(Box::new(e))
}
}
impl GpgBackend {
pub fn new(program: OsString, allow_expired_keys: bool, default_key: String) -> Self {
Self {
program,
allow_expired_keys,
extra_args: vec![],
default_key,
}
}
/// Primarily intended for testing
pub fn with_extra_args(mut self, args: &[OsString]) -> Self {
self.extra_args.extend_from_slice(args);
self
}
pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
let program = settings.get_string("signing.backends.gpg.program")?;
let allow_expired_keys = settings.get_bool("signing.backends.gpg.allow-expired-keys")?;
let default_key = settings.user_email().to_owned();
Ok(Self::new(program.into(), allow_expired_keys, default_key))
}
fn create_command(&self) -> Command {
let mut command = Command::new(&self.program);
// Hide console window on Windows (https://stackoverflow.com/a/60958956)
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
command.creation_flags(CREATE_NO_WINDOW);
}
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.args(&self.extra_args);
command
}
}
impl SigningBackend for GpgBackend {
fn name(&self) -> &str {
"gpg"
}
fn can_read(&self, signature: &[u8]) -> bool {
signature.starts_with(b"-----BEGIN PGP SIGNATURE-----")
}
fn sign(&self, data: &[u8], key: Option<&str>) -> Result<Vec<u8>, SignError> {
let key = key.unwrap_or(&self.default_key);
Ok(run_sign_command(
self.create_command().args(["-abu", key]),
data,
)?)
}
fn verify(&self, data: &[u8], signature: &[u8]) -> Result<Verification, SignError> {
let mut signature_file = tempfile::Builder::new()
.prefix(".jj-gpg-sig-tmp-")
.tempfile()
.map_err(GpgError::Io)?;
signature_file.write_all(signature).map_err(GpgError::Io)?;
signature_file.flush().map_err(GpgError::Io)?;
let sig_path = signature_file.into_temp_path();
let output = run_verify_command(
self.create_command()
.args(["--keyid-format=long", "--status-fd=1", "--verify"])
.arg(&sig_path)
.arg("-"),
data,
)?;
parse_gpg_verify_output(&output, self.allow_expired_keys)
}
}
#[derive(Debug)]
pub struct GpgsmBackend {
program: OsString,
allow_expired_keys: bool,
extra_args: Vec<OsString>,
default_key: String,
}
impl GpgsmBackend {
pub fn new(program: OsString, allow_expired_keys: bool, default_key: String) -> Self {
Self {
program,
allow_expired_keys,
extra_args: vec![],
default_key,
}
}
/// Primarily intended for testing
pub fn with_extra_args(mut self, args: &[OsString]) -> Self {
self.extra_args.extend_from_slice(args);
self
}
pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
let program = settings.get_string("signing.backends.gpgsm.program")?;
let allow_expired_keys = settings.get_bool("signing.backends.gpgsm.allow-expired-keys")?;
let default_key = settings.user_email().to_owned();
Ok(Self::new(program.into(), allow_expired_keys, default_key))
}
fn create_command(&self) -> Command {
let mut command = Command::new(&self.program);
// Hide console window on Windows (https://stackoverflow.com/a/60958956)
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
command.creation_flags(CREATE_NO_WINDOW);
}
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.args(&self.extra_args);
command
}
}
impl SigningBackend for GpgsmBackend {
fn name(&self) -> &str {
"gpgsm"
}
fn can_read(&self, signature: &[u8]) -> bool {
signature.starts_with(b"-----BEGIN SIGNED MESSAGE-----")
}
fn sign(&self, data: &[u8], key: Option<&str>) -> Result<Vec<u8>, SignError> {
let key = key.unwrap_or(&self.default_key);
Ok(run_sign_command(
self.create_command().args(["-abu", key]),
data,
)?)
}
fn verify(&self, data: &[u8], signature: &[u8]) -> Result<Verification, SignError> {
let mut signature_file = tempfile::Builder::new()
.prefix(".jj-gpgsm-sig-tmp-")
.tempfile()
.map_err(GpgError::Io)?;
signature_file.write_all(signature).map_err(GpgError::Io)?;
signature_file.flush().map_err(GpgError::Io)?;
let sig_path = signature_file.into_temp_path();
let output = run_verify_command(
self.create_command()
.args(["--status-fd=1", "--verify"])
.arg(&sig_path)
.arg("-"),
data,
)?;
parse_gpg_verify_output(&output, self.allow_expired_keys)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gpg_verify_invalid_signature_format() {
use assert_matches::assert_matches;
assert_matches!(
parse_gpg_verify_output(b"", true),
Err(SignError::InvalidSignatureFormat)
);
}
#[test]
fn gpg_verify_bad_signature() {
assert_eq!(
parse_gpg_verify_output(b"[GNUPG:] BADSIG 123 456", true).unwrap(),
Verification::new(SigStatus::Bad, Some("123".into()), Some("456".into()))
);
}
#[test]
fn gpg_verify_unknown_signature() {
assert_eq!(
parse_gpg_verify_output(b"[GNUPG:] NO_PUBKEY 123", true).unwrap(),
Verification::new(SigStatus::Unknown, Some("123".into()), None)
);
}
#[test]
fn gpg_verify_good_signature() {
assert_eq!(
parse_gpg_verify_output(b"[GNUPG:] GOODSIG 123 456", true).unwrap(),
Verification::new(SigStatus::Good, Some("123".into()), Some("456".into()))
);
}
#[test]
fn gpg_verify_expired_signature() {
assert_eq!(
parse_gpg_verify_output(b"[GNUPG:] EXPKEYSIG 123 456", true).unwrap(),
Verification::new(SigStatus::Good, Some("123".into()), Some("456".into()))
);
assert_eq!(
parse_gpg_verify_output(b"[GNUPG:] EXPKEYSIG 123 456", false).unwrap(),
Verification::new(SigStatus::Bad, Some("123".into()), Some("456".into()))
);
}
#[test]
fn gpgsm_verify_unknown_signature() {
assert_eq!(
parse_gpg_verify_output(b"[GNUPG:] ERROR verify.findkey 50331657", true).unwrap(),
Verification::unknown(),
);
}
#[test]
fn gpgsm_verify_invalid_signature_format() {
use assert_matches::assert_matches;
assert_matches!(
parse_gpg_verify_output(b"[GNUPG:] ERROR verify.leave 150995087", true),
Err(SignError::InvalidSignatureFormat)
);
}
}