Skip to content

Commit b8637a2

Browse files
committed
add support for external ssh signing
1 parent 7ad8265 commit b8637a2

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
@@ -30,6 +30,7 @@ rayon-core = "1.12"
3030
scopetime = { path = "../scopetime", version = "0.1" }
3131
serde = { version = "1.0", features = ["derive"] }
3232
ssh-key = { version = "0.6.6", features = ["crypto", "encryption"] }
33+
tempfile = "3"
3334
thiserror = "1.0"
3435
unicode-truncate = "1.0"
3536
url = "2.5"

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)]
@@ -154,37 +156,81 @@ impl SignBuilder {
154156
String::from("x509"),
155157
)),
156158
"ssh" => {
157-
let ssh_signer = config
158-
.get_string("user.signingKey")
159-
.ok()
160-
.and_then(|key_path| {
161-
key_path.strip_prefix('~').map_or_else(
162-
|| Some(PathBuf::from(&key_path)),
163-
|ssh_key_path| {
164-
dirs::home_dir().map(|home| {
165-
home.join(
166-
ssh_key_path
167-
.strip_prefix('/')
168-
.unwrap_or(ssh_key_path),
169-
)
170-
})
171-
},
172-
)
173-
})
174-
.ok_or_else(|| {
175-
SignBuilderError::SSHSigningKey(String::from(
176-
"ssh key setting absent",
177-
))
178-
})
179-
.and_then(SSHSign::new)?;
180-
let signer: Box<dyn Sign> = Box::new(ssh_signer);
181-
Ok(signer)
159+
let program = SSHProgram::new(config);
160+
program.into_signer(config)
182161
}
183162
_ => Err(SignBuilderError::InvalidFormat(format)),
184163
}
185164
}
186165
}
187166

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

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

0 commit comments

Comments
 (0)