Skip to content

Commit 99f5dfd

Browse files
committed
add support for external ssh signing
1 parent 687d429 commit 99f5dfd

File tree

2 files changed

+212
-27
lines changed

2 files changed

+212
-27
lines changed

asyncgit/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ serde = { version = "1.0", features = ["derive"] }
3535
ssh-key = { version = "0.6.7", features = ["crypto", "encryption"] }
3636
thiserror = "2.0"
3737
unicode-truncate = "2.0"
38+
tempfile = "3"
3839
url = "2.5"
3940

4041
[dev-dependencies]

asyncgit/src/sync/sign.rs

Lines changed: 211 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! Sign commit data.
22
3+
use git2::Config;
34
use ssh_key::{HashAlg, LineEnding, PrivateKey};
4-
use std::path::PathBuf;
5+
use std::{fmt::Display, path::PathBuf};
6+
use tempfile::NamedTempFile;
57

68
/// Error type for [`SignBuilder`], used to create [`Sign`]'s
79
#[derive(thiserror::Error, Debug)]
@@ -156,37 +158,81 @@ impl SignBuilder {
156158
String::from("x509"),
157159
)),
158160
"ssh" => {
159-
let ssh_signer = config
160-
.get_string("user.signingKey")
161-
.ok()
162-
.and_then(|key_path| {
163-
key_path.strip_prefix('~').map_or_else(
164-
|| Some(PathBuf::from(&key_path)),
165-
|ssh_key_path| {
166-
dirs::home_dir().map(|home| {
167-
home.join(
168-
ssh_key_path
169-
.strip_prefix('/')
170-
.unwrap_or(ssh_key_path),
171-
)
172-
})
173-
},
174-
)
175-
})
176-
.ok_or_else(|| {
177-
SignBuilderError::SSHSigningKey(String::from(
178-
"ssh key setting absent",
179-
))
180-
})
181-
.and_then(SSHSign::new)?;
182-
let signer: Box<dyn Sign> = Box::new(ssh_signer);
183-
Ok(signer)
161+
let program = SSHProgram::new(config);
162+
program.into_signer(config)
184163
}
185164
_ => Err(SignBuilderError::InvalidFormat(format)),
186165
}
187166
}
188167
}
189168

169+
enum SSHProgram {
170+
Default,
171+
SystemBin(PathBuf),
172+
}
173+
174+
impl SSHProgram {
175+
pub fn new(config: &git2::Config) -> Self {
176+
match config.get_string("gpg.ssh.program") {
177+
Err(_) => Self::Default,
178+
Ok(ssh_program) => {
179+
if ssh_program.is_empty() {
180+
return Self::Default;
181+
}
182+
Self::SystemBin(PathBuf::from(ssh_program))
183+
}
184+
}
185+
}
186+
187+
fn into_signer(
188+
self,
189+
config: &git2::Config,
190+
) -> Result<Box<dyn Sign>, SignBuilderError> {
191+
match self {
192+
SSHProgram::Default => {
193+
let ssh_signer = ConfigAccess(config)
194+
.signing_key()
195+
.and_then(SSHSign::new)?;
196+
Ok(Box::new(ssh_signer))
197+
}
198+
SSHProgram::SystemBin(exec_path) => {
199+
let key = ConfigAccess(config).signing_key()?;
200+
Ok(Box::new(ExternalBinSSHSign::new(exec_path, key)))
201+
}
202+
}
203+
}
204+
}
205+
206+
/// wrapper struct for convenience methods over [Config]
207+
struct ConfigAccess<'a>(&'a Config);
208+
209+
impl<'a> ConfigAccess<'a> {
210+
pub fn signing_key(&self) -> Result<PathBuf, SignBuilderError> {
211+
self.0
212+
.get_string("user.signingKey")
213+
.ok()
214+
.and_then(|key_path| {
215+
key_path.strip_prefix('~').map_or_else(
216+
|| Some(PathBuf::from(&key_path)),
217+
|ssh_key_path| {
218+
dirs::home_dir().map(|home| {
219+
home.join(
220+
ssh_key_path
221+
.strip_prefix('/')
222+
.unwrap_or(ssh_key_path),
223+
)
224+
})
225+
},
226+
)
227+
})
228+
.ok_or_else(|| {
229+
SignBuilderError::SSHSigningKey(String::from(
230+
"ssh key setting absent",
231+
))
232+
})
233+
}
234+
}
235+
190236
/// Sign commit data using `OpenPGP`
191237
pub struct GPGSign {
192238
program: String,
@@ -280,6 +326,144 @@ pub struct SSHSign {
280326
secret_key: PrivateKey,
281327
}
282328

329+
enum KeyPathOrLiteral {
330+
Literal(PathBuf),
331+
KeyPath(PathBuf),
332+
}
333+
334+
impl KeyPathOrLiteral {
335+
fn new(buf: PathBuf) -> Self {
336+
match buf.is_file() {
337+
true => KeyPathOrLiteral::KeyPath(buf),
338+
false => KeyPathOrLiteral::Literal(buf),
339+
}
340+
}
341+
}
342+
343+
impl Display for KeyPathOrLiteral {
344+
fn fmt(
345+
&self,
346+
f: &mut std::fmt::Formatter<'_>,
347+
) -> std::fmt::Result {
348+
let buf = match self {
349+
Self::Literal(x) => x,
350+
Self::KeyPath(x) => x,
351+
};
352+
f.write_fmt(format_args!("{}", buf.display()))
353+
}
354+
}
355+
356+
/// Struct which allows for signing via an external binary
357+
pub struct ExternalBinSSHSign {
358+
program_path: PathBuf,
359+
key_path: KeyPathOrLiteral,
360+
#[cfg(test)]
361+
program: String,
362+
#[cfg(test)]
363+
signing_key: String,
364+
}
365+
366+
impl ExternalBinSSHSign {
367+
/// constructs a new instance of the external ssh signer
368+
pub fn new(program_path: PathBuf, key_path: PathBuf) -> Self {
369+
#[cfg(test)]
370+
let program: String = program_path
371+
.file_name()
372+
.unwrap_or_default()
373+
.to_string_lossy()
374+
.into_owned();
375+
376+
let key_path = KeyPathOrLiteral::new(key_path);
377+
378+
#[cfg(test)]
379+
let signing_key = key_path.to_string();
380+
381+
ExternalBinSSHSign {
382+
program_path,
383+
key_path,
384+
#[cfg(test)]
385+
program,
386+
#[cfg(test)]
387+
signing_key,
388+
}
389+
}
390+
}
391+
392+
impl Sign for ExternalBinSSHSign {
393+
fn sign(
394+
&self,
395+
commit: &[u8],
396+
) -> Result<(String, Option<String>), SignError> {
397+
use std::io::Write;
398+
use std::process::{Command, Stdio};
399+
400+
if cfg!(target_os = "windows") {
401+
return Err(SignError::Spawn("External binary signing is only supported on Unix based systems".into()));
402+
}
403+
404+
let mut file = NamedTempFile::new()
405+
.map_err(|e| SignError::Spawn(e.to_string()))?;
406+
407+
let key = match &self.key_path {
408+
KeyPathOrLiteral::Literal(x) => {
409+
write!(file, "{}", x.display()).map_err(|e| {
410+
SignError::WriteBuffer(e.to_string())
411+
})?;
412+
file.path()
413+
}
414+
KeyPathOrLiteral::KeyPath(x) => x.as_path(),
415+
};
416+
417+
let mut c = Command::new(&self.program_path);
418+
c.stdin(Stdio::piped())
419+
.stdout(Stdio::piped())
420+
.stderr(Stdio::piped())
421+
.arg("-Y")
422+
.arg("sign")
423+
.arg("-n")
424+
.arg("git")
425+
.arg("-f")
426+
.arg(key);
427+
428+
let mut child =
429+
c.spawn().map_err(|e| SignError::Spawn(e.to_string()))?;
430+
431+
let mut stdin = child.stdin.take().ok_or(SignError::Stdin)?;
432+
433+
stdin
434+
.write_all(commit)
435+
.map_err(|e| SignError::WriteBuffer(e.to_string()))?;
436+
drop(stdin);
437+
438+
let output = child
439+
.wait_with_output()
440+
.map_err(|e| SignError::Output(e.to_string()))?;
441+
442+
if !output.status.success() {
443+
return Err(SignError::Shellout(format!(
444+
"failed to sign data, program '{}' exited non-zero: {}",
445+
&self.program_path.display(),
446+
std::str::from_utf8(&output.stderr).unwrap_or("[error could not be read from stderr]")
447+
)));
448+
}
449+
450+
let signed_commit = std::str::from_utf8(&output.stdout)
451+
.map_err(|e| SignError::Shellout(e.to_string()))?;
452+
453+
Ok((signed_commit.to_string(), None))
454+
}
455+
456+
#[cfg(test)]
457+
fn program(&self) -> &String {
458+
&self.program
459+
}
460+
461+
#[cfg(test)]
462+
fn signing_key(&self) -> &String {
463+
&self.signing_key
464+
}
465+
}
466+
283467
impl SSHSign {
284468
/// Create new [`SSHDiskKeySign`] for sign.
285469
pub fn new(mut key: PathBuf) -> Result<Self, SignBuilderError> {
@@ -306,7 +490,7 @@ impl SSHSign {
306490
})
307491
} else {
308492
Err(SignBuilderError::SSHSigningKey(
309-
String::from("Currently, we only support a pair of ssh key in disk."),
493+
format!("Currently, we only support a pair of ssh key in disk. Found {:?}", key),
310494
))
311495
}
312496
}

0 commit comments

Comments
 (0)