Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ssh-key/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ rsa = ["dep:rsa", "alloc", "encoding/bigint", "rand_core"]
sha1 = ["dep:sha1"]
tdes = ["cipher/tdes", "encryption"]

[lints]
workspace = true

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
7 changes: 0 additions & 7 deletions ssh-key/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,6 @@ The "Feature" column lists the name of `ssh-key` crate features which can
be enabled to provide full support for the "Keygen", "Sign", and "Verify"
functionality for a particular SSH key algorithm.

## Minimum Supported Rust Version

This crate requires **Rust 1.85** at a minimum.

We may change the MSRV in the future, but it will be accompanied by a minor
version bump.

## License

Licensed under either of:
Expand Down
37 changes: 35 additions & 2 deletions ssh-key/src/algorithm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,14 @@ impl Algorithm {
/// - `sk-ssh-ed25519@openssh.com` (FIDO/U2F key)
///
/// Any other algorithms are mapped to the [`Algorithm::Other`] variant.
///
/// # Errors
/// Returns [`Error::Encoding`] in the event the algorithm name is not known.
pub fn new(id: &str) -> Result<Self> {
Ok(id.parse()?)
}

/// Decode algorithm from the given string identifier as used by
/// the OpenSSH certificate format.
/// Decode algorithm from the given string identifier as used by the OpenSSH certificate format.
///
/// OpenSSH certificate algorithms end in `*-cert-v01@openssh.com`.
/// See [PROTOCOL.certkeys] for more information.
Expand All @@ -163,6 +165,9 @@ impl Algorithm {
/// Any other algorithms are mapped to the [`Algorithm::Other`] variant.
///
/// [PROTOCOL.certkeys]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
///
/// # Errors
/// Returns [`Error::AlgorithmUnknown`] in the event the algorithm is not known.
pub fn new_certificate(id: &str) -> Result<Self> {
match id {
CERT_DSA => Ok(Algorithm::Dsa),
Expand Down Expand Up @@ -193,6 +198,7 @@ impl Algorithm {
}

/// Get the string identifier which corresponds to this algorithm.
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Algorithm::Dsa => SSH_DSA,
Expand Down Expand Up @@ -222,6 +228,7 @@ impl Algorithm {
///
/// [PROTOCOL.certkeys]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
#[cfg(feature = "alloc")]
#[must_use]
pub fn to_certificate_type(&self) -> String {
match self {
Algorithm::Dsa => CERT_DSA,
Expand All @@ -246,21 +253,25 @@ impl Algorithm {
}

/// Is the algorithm DSA?
#[must_use]
pub fn is_dsa(self) -> bool {
self == Algorithm::Dsa
}

/// Is the algorithm ECDSA?
#[must_use]
pub fn is_ecdsa(self) -> bool {
matches!(self, Algorithm::Ecdsa { .. })
}

/// Is the algorithm Ed25519?
#[must_use]
pub fn is_ed25519(self) -> bool {
self == Algorithm::Ed25519
}

/// Is the algorithm RSA?
#[must_use]
pub fn is_rsa(self) -> bool {
matches!(self, Algorithm::Rsa { .. })
}
Expand Down Expand Up @@ -340,11 +351,15 @@ impl EcdsaCurve {
/// - `nistp256`
/// - `nistp384`
/// - `nistp521`
///
/// # Errors
/// Returns [`Error::Encoding`] in the event the algorithm name is not known.
pub fn new(id: &str) -> Result<Self> {
Ok(id.parse()?)
}

/// Get the string identifier which corresponds to this ECDSA elliptic curve.
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
EcdsaCurve::NistP256 => "nistp256",
Expand All @@ -370,6 +385,12 @@ impl AsRef<str> for EcdsaCurve {
}
}

impl From<EcdsaCurve> for Algorithm {
fn from(curve: EcdsaCurve) -> Algorithm {
Algorithm::Ecdsa { curve }
}
}

impl Label for EcdsaCurve {}

impl fmt::Display for EcdsaCurve {
Expand Down Expand Up @@ -410,11 +431,15 @@ impl HashAlg {
///
/// - `sha256`
/// - `sha512`
///
/// # Errors
/// Returns [`Error::Encoding`] in the event the algorithm name is not known.
pub fn new(id: &str) -> Result<Self> {
Ok(id.parse()?)
}

/// Get the string identifier for this hash algorithm.
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
HashAlg::Sha256 => SHA256,
Expand All @@ -423,6 +448,7 @@ impl HashAlg {
}

/// Get the size of a digest produced by this hash function.
#[must_use]
pub const fn digest_size(self) -> usize {
match self {
HashAlg::Sha256 => 32,
Expand All @@ -432,6 +458,7 @@ impl HashAlg {

/// Compute a digest of the given message using this hash function.
#[cfg(feature = "alloc")]
#[must_use]
pub fn digest(self, msg: &[u8]) -> Vec<u8> {
match self {
HashAlg::Sha256 => Sha256::digest(msg).to_vec(),
Expand Down Expand Up @@ -497,11 +524,16 @@ impl KdfAlg {
///
/// # Supported KDF names
/// - `none`
/// - `bcrypt`
///
/// # Errors
/// Returns [`Error::Encoding`] in the event the algorithm name is not known.
pub fn new(kdfname: &str) -> Result<Self> {
Ok(kdfname.parse()?)
}

/// Get the string identifier which corresponds to this algorithm.
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::None => NONE,
Expand All @@ -510,6 +542,7 @@ impl KdfAlg {
}

/// Is the KDF algorithm "none"?
#[must_use]
pub fn is_none(self) -> bool {
self == Self::None
}
Expand Down
15 changes: 12 additions & 3 deletions ssh-key/src/algorithm/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ pub struct AlgorithmName {

impl AlgorithmName {
/// Create a new algorithm identifier.
///
/// # Errors
/// Returns [`LabelError`] in the event the identifier is invalid.
pub fn new(id: impl Into<String>) -> Result<Self, LabelError> {
let id = id.into();
validate_algorithm_id(&id, MAX_ALGORITHM_NAME_LEN)?;
Expand All @@ -45,17 +48,23 @@ impl AlgorithmName {
}

/// Get the string identifier which corresponds to this algorithm name.
#[must_use]
pub fn as_str(&self) -> &str {
&self.id
}

/// Get the string identifier which corresponds to the OpenSSH certificate format.
#[must_use]
#[allow(clippy::missing_panics_doc, reason = "should not panic")]
pub fn certificate_type(&self) -> String {
let (name, domain) = split_algorithm_id(&self.id).expect("format checked in constructor");
format!("{name}{CERT_STR_SUFFIX}@{domain}")
}

/// Create a new [`AlgorithmName`] from an OpenSSH certificate format string identifier.
///
/// # Errors
/// Returns [`LabelError`] in the event the identifier is invalid.
pub fn from_certificate_type(id: &str) -> Result<Self, LabelError> {
validate_algorithm_id(id, MAX_CERT_STR_LEN)?;

Expand All @@ -65,9 +74,9 @@ impl AlgorithmName {
.strip_suffix(CERT_STR_SUFFIX)
.ok_or_else(|| LabelError::new(id))?;

let algorithm_name = format!("{name}@{domain}");

Ok(Self { id: algorithm_name })
Ok(Self {
id: format!("{name}@{domain}"),
})
}
}

Expand Down
42 changes: 31 additions & 11 deletions ssh-key/src/authorized_keys.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
//! Parser for `AuthorizedKeysFile`-formatted data.

use crate::{Error, PublicKey, Result};
use core::str;
use core::{
fmt::{self, Debug},
str,
};

#[cfg(feature = "alloc")]
use {
alloc::string::{String, ToString},
core::fmt,
};
use alloc::string::{String, ToString};

#[cfg(feature = "std")]
use {
Expand Down Expand Up @@ -44,24 +44,27 @@ pub struct AuthorizedKeys<'a> {

impl<'a> AuthorizedKeys<'a> {
/// Create a new parser for the given input buffer.
#[must_use]
pub fn new(input: &'a str) -> Self {
Self {
lines: input.lines(),
}
}

/// Read an [`AuthorizedKeys`] file from the filesystem, returning an
/// [`Entry`] vector on success.
/// Read an [`AuthorizedKeys`] file from the filesystem, returning an [`Entry`] vector on
/// success.
///
/// # Errors
/// - Returns [`Error::Io`] in event of I/O errors reading the file.
/// - Propagates [`Entry`] parsing errors as [`Error::FormatEncoding`].
#[cfg(feature = "std")]
pub fn read_file(path: impl AsRef<Path>) -> Result<Vec<Entry>> {
// TODO(tarcieri): permissions checks
let input = fs::read_to_string(path)?;
AuthorizedKeys::new(&input).collect()
}

/// Get the next line, trimming any comments and trailing whitespace.
///
/// Ignores empty lines.
/// Get the next line, trimming any comments and trailing whitespace. Ignores empty lines.
fn next_line_trimmed(&mut self) -> Option<&'a str> {
loop {
let mut line = self.lines.next()?;
Expand All @@ -81,11 +84,17 @@ impl<'a> AuthorizedKeys<'a> {
}
}

impl Debug for AuthorizedKeys<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AuthorizedKeys").finish_non_exhaustive()
}
}

impl Iterator for AuthorizedKeys<'_> {
type Item = Result<Entry>;

fn next(&mut self) -> Option<Result<Entry>> {
self.next_line_trimmed().map(|line| line.parse())
self.next_line_trimmed().map(str::parse)
}
}

Expand All @@ -103,11 +112,13 @@ pub struct Entry {
impl Entry {
/// Get configuration options for this entry.
#[cfg(feature = "alloc")]
#[must_use]
pub fn config_opts(&self) -> &ConfigOpts {
&self.config_opts
}

/// Get public key for this entry.
#[must_use]
pub fn public_key(&self) -> &PublicKey {
&self.public_key
}
Expand Down Expand Up @@ -206,23 +217,29 @@ pub struct ConfigOpts(String);
#[cfg(feature = "alloc")]
impl ConfigOpts {
/// Parse an options string.
///
/// # Errors
///
pub fn new(string: impl Into<String>) -> Result<Self> {
let ret = Self(string.into());
ret.iter().validate()?;
Ok(ret)
}

/// Borrow the configuration options as a `str`.
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}

/// Are there no configuration options?
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}

/// Iterate over the comma-delimited configuration options.
#[must_use]
pub fn iter(&self) -> ConfigOptsIter<'_> {
ConfigOptsIter(self.as_str())
}
Expand Down Expand Up @@ -259,6 +276,9 @@ impl<'a> ConfigOptsIter<'a> {
/// Create new configuration options iterator.
///
/// Validates that the options are well-formed.
///
/// # Errors
/// Returns [`Error::Encoding`] in the event of encoding errors.
pub fn new(s: &'a str) -> Result<Self> {
let ret = Self(s);
ret.clone().validate()?;
Expand Down
Loading
Loading