Skip to content

Commit

Permalink
ssh-key: add DotSsh
Browse files Browse the repository at this point in the history
Adds a type for interacting with a user's `~/.ssh` directory, including
the following functionality:

- Locating the configuration file
- Iterating over private keys
- Iterating over public keys
- Finding a private key with a given fingerprint
- Finding a public key with a given fingerprint
  • Loading branch information
tarcieri committed Jul 29, 2024
1 parent d3cd661 commit d47d2f8
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 1 deletion.
83 changes: 83 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion ssh-key/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ bcrypt-pbkdf = { version = "=0.11.0-pre.1", optional = true, default-features =
bigint = { package = "num-bigint-dig", version = "0.8", optional = true, default-features = false }
dsa = { version = "=0.7.0-pre.0", optional = true, default-features = false }
ed25519-dalek = { version = "=2.2.0-pre", optional = true, default-features = false }
home = { version = "0.5", optional = true }
p256 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] }
p384 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] }
p521 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] }
Expand Down Expand Up @@ -58,7 +59,8 @@ std = [
"p521?/std",
"rsa?/std",
"sec1?/std",
"signature/std"
"signature/std",
"dep:home"
]

crypto = ["ed25519", "p256", "p384", "p521", "rsa"] # NOTE: `dsa` is obsolete/weak
Expand Down
117 changes: 117 additions & 0 deletions ssh-key/src/dot_ssh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! `~/.ssh` support.

use crate::{Fingerprint, PrivateKey, PublicKey, Result};
use std::{
fs::{self, ReadDir},
path::{Path, PathBuf},
};

/// `~/.ssh` directory support (or similarly structured directories).
#[derive(Clone, Eq, PartialEq)]
pub struct DotSsh {
path: PathBuf,
}

impl DotSsh {
/// Open `~/.ssh` if the home directory can be located.
///
/// Returns `None` if the home directory couldn't be located.
pub fn new() -> Option<Self> {
home::home_dir().map(|path| Self::open(path.join(".ssh")))
}

/// Open a `~/.ssh`-structured directory.
///
/// Does not verify that the directory exists or has the right file permissions.
///
/// Attempts to canonicalize the path once opened.
pub fn open(path: impl Into<PathBuf>) -> Self {
let path = path.into();
Self {
path: path.canonicalize().unwrap_or(path),
}
}

/// Get the path to the `~/.ssh` directory (or whatever [`DotSsh::open`] was called with).
pub fn path(&self) -> &Path {
&self.path
}

/// Get the path to the `~/.ssh/config` configuration file. Does not check if it exists.
pub fn config_path(&self) -> PathBuf {
self.path.join("config")
}

/// Iterate over the private keys in the `.ssh` directory.
pub fn private_keys(&self) -> Result<impl Iterator<Item = PrivateKey>> {
Ok(PrivateKeysIter {
read_dir: fs::read_dir(&self.path)?,
})
}

/// Find a private key whose public key has the given key fingerprint.
pub fn private_key_with_fingerprint(&self, fingerprint: Fingerprint) -> Option<PrivateKey> {
self.private_keys()
.ok()?
.find(|key| key.public_key().fingerprint(fingerprint.algorithm()) == fingerprint)
}

/// Iterate over the public keys in the `.ssh` directory.
pub fn public_keys(&self) -> Result<impl Iterator<Item = PublicKey>> {
Ok(PublicKeysIter {
read_dir: fs::read_dir(&self.path)?,
})
}

/// Find a public key with the given key fingerprint.
pub fn public_key_with_fingerprint(&self, fingerprint: Fingerprint) -> Option<PublicKey> {
self.public_keys()
.ok()?
.find(|key| key.fingerprint(fingerprint.algorithm()) == fingerprint)
}
}

impl Default for DotSsh {
/// Calls [`DotSsh::new`] and panics if the home directory could not be located.
fn default() -> Self {
Self::new().expect("home directory could not be located")
}
}

/// Iterator over the private keys in the `.ssh` directory.
pub struct PrivateKeysIter {
read_dir: ReadDir,
}

impl Iterator for PrivateKeysIter {
type Item = PrivateKey;

fn next(&mut self) -> Option<Self::Item> {
loop {
let entry = self.read_dir.next()?.ok()?;

if let Ok(key) = PrivateKey::read_openssh_file(&entry.path()) {
return Some(key);
}
}
}
}

/// Iterator over the public keys in the `.ssh` directory.
pub struct PublicKeysIter {
read_dir: ReadDir,
}

impl Iterator for PublicKeysIter {
type Item = PublicKey;

fn next(&mut self) -> Option<Self::Item> {
loop {
let entry = self.read_dir.next()?.ok()?;

if let Ok(key) = PublicKey::read_openssh_file(&entry.path()) {
return Some(key);
}
}
}
}
5 changes: 5 additions & 0 deletions ssh-key/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ mod error;
mod fingerprint;
mod kdf;

#[cfg(feature = "std")]
mod dot_ssh;
#[cfg(feature = "alloc")]
mod mpint;
#[cfg(feature = "alloc")]
Expand Down Expand Up @@ -191,3 +193,6 @@ pub use sec1;

#[cfg(feature = "rand_core")]
pub use rand_core;

#[cfg(feature = "std")]
pub use crate::dot_ssh::DotSsh;
52 changes: 52 additions & 0 deletions ssh-key/tests/dot_ssh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! Tests for `~/.ssh` support. Uses the `tests/examples` directory instead.

#![cfg(feature = "std")]

use hex_literal::hex;
use ssh_key::{Algorithm, DotSsh, Fingerprint};

/// Open `.ssh` using the `test/examples`.
fn dot_ssh() -> DotSsh {
DotSsh::open("tests/examples")
}

#[test]
fn path_round_trip() {
let dot_ssh = dot_ssh();
dbg!(dot_ssh.path());
assert!(dot_ssh.path().ends_with("tests/examples"));
}

#[test]
fn private_keys() {
let dot_ssh = dot_ssh();
assert_eq!(dot_ssh.private_keys().unwrap().count(), 20);
}

#[test]
fn private_key_with_fingerprint() {
let fingerprint = Fingerprint::Sha256(hex!(
"5025222ebecf8ecf7014524c0c1c8b81cdcdaed754df8e0e814338e7064f7084"
));

let dot_ssh = dot_ssh();
let key = dot_ssh.private_key_with_fingerprint(fingerprint).unwrap();
assert_eq!(key.algorithm(), Algorithm::Ed25519);
}

#[test]
fn public_keys() {
let dot_ssh = dot_ssh();
assert_eq!(dot_ssh.public_keys().unwrap().count(), 12);
}

#[test]
fn public_key_with_fingerprint() {
let fingerprint = Fingerprint::Sha256(hex!(
"5025222ebecf8ecf7014524c0c1c8b81cdcdaed754df8e0e814338e7064f7084"
));

let dot_ssh = dot_ssh();
let key = dot_ssh.public_key_with_fingerprint(fingerprint).unwrap();
assert_eq!(key.algorithm(), Algorithm::Ed25519);
}

0 comments on commit d47d2f8

Please sign in to comment.