Repository: gitui-org/gitui (language: rust)

Issue:
Sign git commits with SSH keys
**Is your feature request related to a problem? Please describe.**
I would like to be able to sign git commits with SSH keys from gitui.

- https://blog.dbrgn.ch/2021/11/16/git-ssh-signatures/
- https://calebhearth.com/sign-git-with-ssh
- https://lobste.rs/s/l5h2ct/git_ssh_signing_add_commit_tag_signing

This is a new feature that seems to only work properly with OpenSSH 8.8 and above, which can make it challenging to test on more stable OSes; but if you are able to use it, it seems painless compared to GPG. Because it is new, it also has poor support on GitHub and other code forges, but I expect [that will change](https://github.com/github/feedback/discussions/7744#discussioncomment-1794438).

It may also seem premature to consider, given that [GPG signing isn't supported in gitui yet](https://github.com/extrawurst/gitui/issues/97); on the other hand, now may be a good time to consider the user interface to make it easy to support signing with SSH keys as well as GPG keys in the future.


Discussion / hints:
GitHub now supports verifying commits using SSH keys: https://github.blog/changelog/2022-08-23-ssh-commit-verification-now-supported/
this feature needs to be supported by upstream `libgit2` first
I did try to implement GPG signing in the past as part of #97, and it turned out to be very difficult, due to not having a native Rust crate for the GPG part. Basically, it complicated builds on Windows due to some extra C dependencies and licensing might be problematic too.

So I recently tried out to implement SSH signing instead, as I discovered the relatively new `ssh-key` crate from the RustCrypto group. After [a little forth and back](https://github.com/RustCrypto/SSH/issues/15) with them, they added an implementation for SSH signing to it, and with the current `0.5.0-rc.0`, it's very easy to sign commits.

The workflow is exactly the same as with GPG signing, so we wouldn't have to wait for `libgit2` to integrate that.  

Here is a very minimal example:

```toml
# Cargo.toml

[package]
name = "gitsign"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.66"
dirs = "4.0.0"
git2 = "0.15.0"
ssh-key = { version = "0.5.0-rc.0", features = ["ed25519", "encryption"] }
```

```rust
// src/main.rs

use std::{env, fs};

use anyhow::Result;
use git2::Repository;
use ssh_key::{HashAlg, LineEnding, PrivateKey};

fn main() -> Result<()> {
    // Create a new repo at ./tmp
    let dir = env::current_dir()?.join("tmp");
    fs::remove_dir_all(&dir).ok();
    fs::create_dir_all(&dir)?;

    let repo = Repository::init(&dir)?;

    // Prepare an empty commit
    let mut index = repo.index()?;
    let tree = index.write_tree()?;
    let tree = repo.find_tree(tree)?;

    let signature = repo.signature()?;

    let content = repo.commit_create_buffer(&signature, &signature, "Initial commit", &tree, &[])?;
    let content = String::from_utf8(content.to_vec())?;

    // Sign with SSH key
    let ssh_dir = dirs::home_dir().unwrap().join(".ssh");

    let key = fs::read(ssh_dir.join("id_ed25519"))?;
    let key = PrivateKey::from_openssh(key)?.decrypt("<YOUR PRIVATE KEY PASSWORD>")?;

    let sig = key
        .sign("git", HashAlg::Sha256, content.as_bytes())?
        .to_pem(LineEnding::default())?;

    // Perform actual commit
    let commit = repo.commit_signed(&content, &sig, None)?;
    let commit = repo.find_commit(commit)?;
    repo.branch("main", &commit, true)?;

    Ok(())
}
```

After running it, you can go to the `./tmp` folder and verify the signature with `git log --show-signature`.

@extrawurst with that, what do you think about implementing SSH signing first, before continuing on the GPG signing path, which seems rather stuck?

There is just one problem: Getting the private key. The Git configuration allows different ways of specifying the key. It can be:
- A public key handle, which means Git will talk with the SSH agent (through `ssh-keygen` which does all the signing). This seems the most preferred setup.
- Direct path to the private key. This one we could support as a first implementation.
- A program which will respond with the public key handle to use, and then Git will talk to the SSH agent.
- No key config at all, which means it'll interactively ask for the right handle or pick the first key returned by the SSH agent (based on the config).

Given all these possible options, it might even be easier to just do the same as Git and shell out to `ssh-keygen` instead of natively implementing it 🤔 
> what do you think about implementing SSH signing first

absolutely welcome. and as usual I am for implementing the first version (MVP) of it as simple as possible and then iterate over it. so I assume a direct path to the key to be used via program param would be the way to go. after that we should look into querying the agent and only afterwards go for the most complicated version where we need to have a UI for the user to choose from key options.
This issue has been automatically marked as stale because it has not had any activity half a year. It will be closed in 14 days if no further activity occurs. Thank you for your contributions.

Still relevant.
https://github.com/libgit2/libgit2/pull/6617 - soon supported by upstream
Also really interesting read about another tool using libgit2-only without shellout to support ssh signing: https://blog.gitbutler.com/signing-commits-in-git-explained/
> [libgit2/libgit2#6617](https://github.com/libgit2/libgit2/pull/6617) - soon supported by upstream

Since this is now supported upstream in libgit2, will gitui automatically respect the `commit.gpgsign` config option for SSH signing?
@SaadiSave there was no release of `libgit2` after feature was merged yet.
Hi @dnaka91 
I try to solve this with your suggestion, and I realize the third input of `commit_signed` should not be `None`. None is for `gpgsig`, but `sshsig` or `SSHSIG` does not work here.

Do you have any idea about this?  Many thanks.

Despite this now being ssh signature signing, it's reusing the old header.

You can see in the above link that it's still under the `gpgsig` header and in the git source code it's the only header used for signatures.

Makes sense because even the settings are still all the same and just swap the underlying signing program (more or less).
I got it.  There is still a problem here.
There is no parents field in commit_signed, so the commit is generated but not after the head of the current branch.  Could you help me to fix this?  Many thanks.
Also, I am not sure the None is valid for `signature_field`, because I will get a bug from gpg-interface.

```txt
BUG: gpg-interface.c:284: bad signature '-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgTjgfFOnWZfMF8CBzrp50Vvs/E6
aHZKmKF2GhZRjHBiUAAAADZ2l0AAAAAAAAAAZzaGEyNTYAAABTAAAAC3NzaC1lZDI1NTE5
AAAAQADCbMrw19YOZSdJxhjCgJvWIRpHT842Wu8xhITmurehYcHtn9pFcibpsx47V0D73f
1zRD4DptJ7v7t9IbKx8Qk=
-----END SSH SIGNATURE-----

'
(END)[1]    24547 abort (core dumped)  git log --show-signature
```
I noticed a small difference with the regular Git signatures, and that is a trailing newline in the signature. Don't know about gpg-interface so that might be a thing?

Just for fun I tried implementing the same thing with `gix` instead of `git2` and put it all into a repo. Works fine for me with both crates and I can verify it with my Git CLI :shrug: 

The repo is here: https://github.com/dnaka91/gitsign

Could it be that your Git CLI is a bit dated and doesn't have the SSH signing feature yet?

---

Edit: I'm using Git 2.43.0 which is the latest version at the time of writing.
Hi @extrawurst 
There are things we need to think about before completing the PR.  I have checked this signed commit with the ssh key will show verified tag in GitHub.

- There will be a duplicate syn crate in the dependency, for 1.0 and 2.0
- There will be a warning with the crate for RSA SSH key, but it breaks the current CI.
- The key path is passed from options, and we do not need additional TUI on it, because users do not change the signing key for different commits.

It seems hard to avoid these.  Will the defects be accepted?

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf1de5be09..528606d216 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 * more version info in `gitui -V` and `help popup` (including git hash)
 * support `core.commitChar` filtering [[@concelare](https://github.com/concelare)] ([#2136](https://github.com/extrawurst/gitui/issues/2136))
 * allow reset in branch popup ([#2170](https://github.com/extrawurst/gitui/issues/2170))
+* support sign commits with ssh key in the disk when `user.signingKey` and `gpg.format = ssh` of gitconfig are set(ssh-agent isn't yet supported)  ([#1149](https://github.com/extrawurst/gitui/issues/1149))
 
 ### Changed
 * Make info and error message popups scrollable [[@MichaelAug](https://github.com/MichaelAug)] ([#1138](https://github.com/extrawurst/gitui/issues/1138))
diff --git a/Cargo.lock b/Cargo.lock
index 761970781a..26ab8aa17f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,41 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
 [[package]]
 name = "ahash"
 version = "0.8.11"
@@ -119,6 +154,7 @@ version = "0.25.1"
 dependencies = [
  "bitflags 2.5.0",
  "crossbeam-channel",
+ "dirs",
  "easy-cast",
  "env_logger",
  "fuzzy-matcher",
@@ -133,6 +169,7 @@ dependencies = [
  "scopetime",
  "serde",
  "serial_test",
+ "ssh-key",
  "tempfile",
  "thiserror",
  "unicode-truncate",
@@ -160,12 +197,35 @@ dependencies = [
  "rustc-demangle",
 ]
 
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
 [[package]]
 name = "base64"
 version = "0.21.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
 
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
+[[package]]
+name = "bcrypt-pbkdf"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2"
+dependencies = [
+ "blowfish",
+ "pbkdf2",
+ "sha2",
+]
+
 [[package]]
 name = "bincode"
 version = "1.3.3"
@@ -205,6 +265,34 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blowfish"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
+dependencies = [
+ "byteorder",
+ "cipher",
+]
+
 [[package]]
 name = "bugreport"
 version = "0.5.0"
@@ -231,6 +319,12 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
 [[package]]
 name = "bytesize"
 version = "1.3.0"
@@ -243,6 +337,15 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
 
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
 [[package]]
 name = "cc"
 version = "1.0.90"
@@ -259,6 +362,17 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "chacha20"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
 [[package]]
 name = "chrono"
 version = "0.4.37"
@@ -271,6 +385,16 @@ dependencies = [
  "windows-targets 0.52.0",
 ]
 
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
 [[package]]
 name = "clap"
 version = "4.5.4"
@@ -304,12 +428,27 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
 [[package]]
 name = "core-foundation-sys"
 version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
 
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "crc32fast"
 version = "1.4.0"
@@ -379,6 +518,64 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "platforms",
+ "rustc_version",
+ "subtle",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "dashmap"
 version = "5.5.3"
@@ -392,6 +589,16 @@ dependencies = [
  "parking_lot_core",
 ]
 
+[[package]]
+name = "der"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
 [[package]]
 name = "deranged"
 version = "0.3.11"
@@ -407,6 +614,18 @@ version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
 
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
 [[package]]
 name = "dirs"
 version = "5.0.1"
@@ -434,12 +653,66 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "10936778145f3bea71fd9bf61332cce28c28e96a380714f7ab34838b80733fd6"
 
+[[package]]
+name = "ecdsa"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+dependencies = [
+ "der",
+ "digest",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+ "spki",
+]
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "sha2",
+ "subtle",
+]
+
 [[package]]
 name = "either"
 version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
 
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "pkcs8",
+ "rand_core",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "env_filter"
 version = "0.1.0"
@@ -495,6 +768,22 @@ version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
 
+[[package]]
+name = "ff"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
+dependencies = [
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f"
+
 [[package]]
 name = "filetime"
 version = "0.2.23"
@@ -635,6 +924,17 @@ dependencies = [
  "thread_local",
 ]
 
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+ "zeroize",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.12"
@@ -656,6 +956,16 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "ghash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
 [[package]]
 name = "gimli"
 version = "0.28.1"
@@ -766,6 +1076,17 @@ dependencies = [
  "which",
 ]
 
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core",
+ "subtle",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.14.3"
@@ -782,6 +1103,15 @@ version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
 [[package]]
 name = "home"
 version = "0.5.9"
@@ -866,6 +1196,16 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
 [[package]]
 name = "invalidstring"
 version = "0.1.3"
@@ -937,6 +1277,9 @@ name = "lazy_static"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+dependencies = [
+ "spin",
+]
 
 [[package]]
 name = "libc"
@@ -958,6 +1301,12 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "libm"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+
 [[package]]
 name = "libredox"
 version = "0.0.1"
@@ -1083,12 +1432,49 @@ dependencies = [
  "notify",
 ]
 
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
 [[package]]
 name = "num-conv"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
 
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.2.18"
@@ -1096,6 +1482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
 dependencies = [
  "autocfg",
+ "libm",
 ]
 
 [[package]]
@@ -1135,6 +1522,12 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
 [[package]]
 name = "openssl-probe"
 version = "0.1.5"
@@ -1169,6 +1562,44 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
 
+[[package]]
+name = "p256"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2",
+]
+
+[[package]]
+name = "p384"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2",
+]
+
+[[package]]
+name = "p521"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
+dependencies = [
+ "base16ct",
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "rand_core",
+ "sha2",
+]
+
 [[package]]
 name = "parking_lot"
 version = "0.12.1"
@@ -1198,6 +1629,24 @@ version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
 
+[[package]]
+name = "pbkdf2"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
 [[package]]
 name = "percent-encoding"
 version = "2.3.1"
@@ -1234,18 +1683,74 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
 [[package]]
 name = "pkg-config"
 version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
 
+[[package]]
+name = "platforms"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7"
+
+[[package]]
+name = "poly1305"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+dependencies = [
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "polyval"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
 [[package]]
 name = "powerfmt"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
 
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
 [[package]]
 name = "pretty_assertions"
 version = "1.4.0"
@@ -1256,6 +1761,15 @@ dependencies = [
  "yansi",
 ]
 
+[[package]]
+name = "primeorder"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
+dependencies = [
+ "elliptic-curve",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.79"
@@ -1274,6 +1788,35 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
 [[package]]
 name = "ratatui"
 version = "0.24.0"
@@ -1362,6 +1905,16 @@ version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
 
+[[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac",
+ "subtle",
+]
+
 [[package]]
 name = "ron"
 version = "0.8.1"
@@ -1374,12 +1927,42 @@ dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "rsa"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "sha2",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "rustc-demangle"
 version = "0.1.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
 
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
 [[package]]
 name = "rustix"
 version = "0.38.32"
@@ -1427,6 +2010,26 @@ dependencies = [
  "log",
 ]
 
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
+
 [[package]]
 name = "serde"
 version = "1.0.197"
@@ -1483,6 +2086,17 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
 [[package]]
 name = "shell-escape"
 version = "0.1.5"
@@ -1528,6 +2142,16 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
 [[package]]
 name = "simplelog"
 version = "0.12.2"
@@ -1559,6 +2183,73 @@ version = "1.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
 
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "ssh-cipher"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
+dependencies = [
+ "aes",
+ "aes-gcm",
+ "cbc",
+ "chacha20",
+ "cipher",
+ "ctr",
+ "poly1305",
+ "ssh-encoding",
+ "subtle",
+]
+
+[[package]]
+name = "ssh-encoding"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
+dependencies = [
+ "base64ct",
+ "pem-rfc7468",
+ "sha2",
+]
+
+[[package]]
+name = "ssh-key"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b71299a724c8d84956caaf8fc3b3ea57c3587fe2d0b800cd0dc1f3599905d7e"
+dependencies = [
+ "bcrypt-pbkdf",
+ "ed25519-dalek",
+ "num-bigint-dig",
+ "p256",
+ "p384",
+ "p521",
+ "rand_core",
+ "rsa",
+ "sec1",
+ "sha2",
+ "signature",
+ "ssh-cipher",
+ "ssh-encoding",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "strsim"
 version = "0.11.0"
@@ -1607,6 +2298,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "subtle"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+
 [[package]]
 name = "syn"
 version = "2.0.49"
@@ -1759,6 +2456,12 @@ dependencies = [
  "syntect",
 ]
 
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
 [[package]]
 name = "unicode-bidi"
 version = "0.3.15"
@@ -1801,6 +2504,16 @@ version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
 
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
 [[package]]
 name = "url"
 version = "2.5.0"
@@ -2115,3 +2828,9 @@ dependencies = [
  "quote",
  "syn",
 ]
+
+[[package]]
+name = "zeroize"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml
index f2f55aeb59..1466e4c1bc 100644
--- a/asyncgit/Cargo.toml
+++ b/asyncgit/Cargo.toml
@@ -14,6 +14,7 @@ keywords = ["git"]
 [dependencies]
 bitflags = "2"
 crossbeam-channel = "0.5"
+dirs = "5.0"
 easy-cast = "0.5"
 fuzzy-matcher = "0.3"
 git2 = "0.18"
@@ -27,6 +28,7 @@ rayon = "1.10"
 rayon-core = "1.12"
 scopetime = { path = "../scopetime", version = "0.1" }
 serde = { version = "1.0", features = ["derive"] }
+ssh-key = { version = "0.6.4", features = ["crypto", "encryption"] }
 thiserror = "1.0"
 unicode-truncate = "0.2"
 url = "2.5"
diff --git a/asyncgit/src/sync/commit.rs b/asyncgit/src/sync/commit.rs
index 0792aa2121..2b1d78fea2 100644
--- a/asyncgit/src/sync/commit.rs
+++ b/asyncgit/src/sync/commit.rs
@@ -102,8 +102,6 @@ pub fn commit(repo_path: &RepoPath, msg: &str) -> Result<CommitId> {
 		.get_bool("commit.gpgsign")
 		.unwrap_or(false)
 	{
-		use crate::sync::sign::Sign;
-
 		let buffer = repo.commit_create_buffer(
 			&signature,
 			&signature,
@@ -116,12 +114,12 @@ pub fn commit(repo_path: &RepoPath, msg: &str) -> Result<CommitId> {
 			SignError::Shellout("utf8 conversion error".to_string())
 		})?;
 
-		let sign = SignBuilder::from_gitconfig(&repo, &config)?;
-		let (signature, signature_field) = sign.sign(&buffer)?;
+		let signer = SignBuilder::from_gitconfig(&repo, &config)?;
+		let (signature, signature_field) = signer.sign(&buffer)?;
 		let commit_id = repo.commit_signed(
 			commit,
 			&signature,
-			Some(&signature_field),
+			signature_field.as_deref(),
 		)?;
 
 		// manually advance to the new commit ID
diff --git a/asyncgit/src/sync/sign.rs b/asyncgit/src/sync/sign.rs
index d4aa31a62c..53effe2477 100644
--- a/asyncgit/src/sync/sign.rs
+++ b/asyncgit/src/sync/sign.rs
@@ -1,5 +1,8 @@
 //! Sign commit data.
 
+use ssh_key::{HashAlg, LineEnding, PrivateKey};
+use std::path::PathBuf;
+
 /// Error type for [`SignBuilder`], used to create [`Sign`]'s
 #[derive(thiserror::Error, Debug)]
 pub enum SignBuilderError {
@@ -11,6 +14,10 @@ pub enum SignBuilderError {
 	#[error("Failed to retrieve 'user.signingkey' from the git configuration: {0}")]
 	GPGSigningKey(String),
 
+	/// The SSH signing key could
+	#[error("Failed to retrieve 'user.signingkey' from the git configuration: {0}")]
+	SSHSigningKey(String),
+
 	/// No signing signature could be built from the configuration data present
 	#[error("Failed to build signing signature: {0}")]
 	Signature(String),
@@ -61,7 +68,7 @@ pub trait Sign {
 	fn sign(
 		&self,
 		commit: &[u8],
-	) -> Result<(String, String), SignError>;
+	) -> Result<(String, Option<String>), SignError>;
 
 	#[cfg(test)]
 	fn program(&self) -> &String;
@@ -96,7 +103,7 @@ impl SignBuilder {
 	pub fn from_gitconfig(
 		repo: &git2::Repository,
 		config: &git2::Config,
-	) -> Result<impl Sign, SignBuilderError> {
+	) -> Result<Box<dyn Sign>, SignBuilderError> {
 		let format = config
 			.get_string("gpg.format")
 			.unwrap_or_else(|_| "openpgp".to_string());
@@ -138,17 +145,41 @@ impl SignBuilder {
 						)
 					})?;
 
-				Ok(GPGSign {
+				Ok(Box::new(GPGSign {
 					program,
 					signing_key,
-				})
+				}))
 			}
 			"x509" => Err(SignBuilderError::MethodNotImplemented(
 				String::from("x509"),
 			)),
-			"ssh" => Err(SignBuilderError::MethodNotImplemented(
-				String::from("ssh"),
-			)),
+			"ssh" => {
+				let ssh_signer = config
+					.get_string("user.signingKey")
+					.ok()
+					.and_then(|key_path| {
+						key_path.strip_prefix('~').map_or_else(
+							|| Some(PathBuf::from(&key_path)),
+							|ssh_key_path| {
+								dirs::home_dir().map(|home| {
+									home.join(
+										ssh_key_path
+											.strip_prefix('/')
+											.unwrap_or(ssh_key_path),
+									)
+								})
+							},
+						)
+					})
+					.ok_or_else(|| {
+						SignBuilderError::SSHSigningKey(String::from(
+							"ssh key setting absent",
+						))
+					})
+					.and_then(SSHSign::new)?;
+				let signer: Box<dyn Sign> = Box::new(ssh_signer);
+				Ok(signer)
+			}
 			_ => Err(SignBuilderError::InvalidFormat(format)),
 		}
 	}
@@ -174,7 +205,7 @@ impl Sign for GPGSign {
 	fn sign(
 		&self,
 		commit: &[u8],
-	) -> Result<(String, String), SignError> {
+	) -> Result<(String, Option<String>), SignError> {
 		use std::io::Write;
 		use std::process::{Command, Stdio};
 
@@ -224,7 +255,7 @@ impl Sign for GPGSign {
 		let signed_commit = std::str::from_utf8(&output.stdout)
 			.map_err(|e| SignError::Shellout(e.to_string()))?;
 
-		Ok((signed_commit.to_string(), "gpgsig".to_string()))
+		Ok((signed_commit.to_string(), Some("gpgsig".to_string())))
 	}
 
 	#[cfg(test)]
@@ -238,6 +269,72 @@ impl Sign for GPGSign {
 	}
 }
 
+/// Sign commit data using `SSHDiskKeySign`
+pub struct SSHSign {
+	#[cfg(test)]
+	program: String,
+	#[cfg(test)]
+	key_path: String,
+	secret_key: PrivateKey,
+}
+
+impl SSHSign {
+	/// Create new [`SSHDiskKeySign`] for sign.
+	pub fn new(mut key: PathBuf) -> Result<Self, SignBuilderError> {
+		key.set_extension("");
+		if key.is_file() {
+			#[cfg(test)]
+			let key_path = format!("{}", &key.display());
+			std::fs::read(key)
+				.ok()
+				.and_then(|bytes| {
+					PrivateKey::from_openssh(bytes).ok()
+				})
+				.map(|secret_key| Self {
+					#[cfg(test)]
+					program: "ssh".to_string(),
+					#[cfg(test)]
+					key_path,
+					secret_key,
+				})
+				.ok_or_else(|| {
+					SignBuilderError::SSHSigningKey(String::from(
+						"Fail to read the private key for sign.",
+					))
+				})
+		} else {
+			Err(SignBuilderError::SSHSigningKey(
+				String::from("Currently, we only support a pair of ssh key in disk."),
+			))
+		}
+	}
+}
+
+impl Sign for SSHSign {
+	fn sign(
+		&self,
+		commit: &[u8],
+	) -> Result<(String, Option<String>), SignError> {
+		let sig = self
+			.secret_key
+			.sign("git", HashAlg::Sha256, commit)
+			.map_err(|err| SignError::Spawn(err.to_string()))?
+			.to_pem(LineEnding::LF)
+			.map_err(|err| SignError::Spawn(err.to_string()))?;
+		Ok((sig, None))
+	}
+
+	#[cfg(test)]
+	fn program(&self) -> &String {
+		&self.program
+	}
+
+	#[cfg(test)]
+	fn signing_key(&self) -> &String {
+		&self.key_path
+	}
+}
+
 #[cfg(test)]
 mod tests {
 	use super::*;
@@ -319,6 +416,24 @@ mod tests {
 			SignBuilder::from_gitconfig(&repo, &repo.config()?)?;
 
 		assert_eq!("FFAA", sign.signing_key());
+		Ok(())
+	}
+
+	#[test]
+	fn test_ssh_program_configs() -> Result<()> {
+		let (_tmp_dir, repo) = repo_init_empty()?;
+
+		{
+			let mut config = repo.config()?;
+			config.set_str("gpg.program", "ssh")?;
+			config.set_str("user.signingKey", "/tmp/key.pub")?;
+		}
+
+		let sign =
+			SignBuilder::from_gitconfig(&repo, &repo.config()?)?;
+
+		assert_eq!("ssh", sign.program());
+		assert_eq!("/tmp/key.pub", sign.signing_key());
 
 		Ok(())
 	}
diff --git a/deny.toml b/deny.toml
index a0b2902cab..d42a3f8dae 100644
--- a/deny.toml
+++ b/deny.toml
@@ -1,9 +1,14 @@
 [licenses]
 unlicensed = "deny"
-allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "CC0-1.0", "ISC", "MPL-2.0"]
+allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "CC0-1.0", "ISC", "MPL-2.0"]
 copyleft = "warn"
 default = "deny"
 
+[advisories]
+# No fix for RSA, and this is a dependency from ssh_key crate to handle rsa ssh key.
+# https://rustsec.org/advisories/RUSTSEC-2023-0071
+ignore = ["RUSTSEC-2023-0071"]
+
 [[licenses.exceptions]]
 allow = ["Unicode-DFS-2016"]
 name = "unicode-ident"


Test patch: