Skip to content

Commit

Permalink
ssh-key: support additional SSH key algorithms (#136)
Browse files Browse the repository at this point in the history
This introduces an additional "catch-all" key type to ssh-key to support additional SSH key algorithms, as described in #135.

Adds an `AlgorithmName` type for additional algorithm names. The syntax
for additional algorithm names is described in section 6 of RFC4251.

Adds a new `Algorithm::Other` variant for representing additional
algorithms.

Breaking changes: `Algorithm::as_str`, `Algorithm::as_certificate_str`
now return `&str` instead of `&static str`.

Adds the `Keypair::Other` and `KeyData::Other` variants for
storing the key material of keys that use a custom algorithm.

Adds the `OpaqueKeypair` and `OpaqueKeyData` types for representing keys
meant to be used with an algorithm unknown to this crate (e.g. custom
algorithms). They are said to be opaque, because the meaning of their
underlying byte representation is not specified.
  • Loading branch information
gabi-250 authored Jul 22, 2023
1 parent 2b963f2 commit fef4dca
Show file tree
Hide file tree
Showing 17 changed files with 597 additions and 13 deletions.
2 changes: 1 addition & 1 deletion ssh-key/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ default = ["ecdsa", "rand_core", "std"]
alloc = [
"encoding/alloc",
"signature/alloc",
"zeroize/alloc"
"zeroize/alloc",
]
std = [
"alloc",
Expand Down
30 changes: 27 additions & 3 deletions ssh-key/src/algorithm.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! Algorithm support.

#[cfg(feature = "alloc")]
mod name;

use crate::{Error, Result};
use core::{fmt, str};
use encoding::{Label, LabelError};
Expand All @@ -10,6 +13,9 @@ use {
sha2::{Digest, Sha256, Sha512},
};

#[cfg(feature = "alloc")]
pub use name::AlgorithmName;

/// bcrypt-pbkdf
const BCRYPT: &str = "bcrypt";

Expand Down Expand Up @@ -80,7 +86,7 @@ const SK_SSH_ED25519: &str = "[email protected]";
///
/// This type provides a registry of supported digital signature algorithms
/// used for SSH keys.
#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Algorithm {
/// Digital Signature Algorithm
Expand Down Expand Up @@ -113,6 +119,10 @@ pub enum Algorithm {

/// FIDO/U2F key with Ed25519
SkEd25519,

/// Other
#[cfg(feature = "alloc")]
Other(AlgorithmName),
}

impl Algorithm {
Expand All @@ -127,6 +137,8 @@ impl Algorithm {
/// - `ssh-rsa`
/// - `[email protected]` (FIDO/U2F key)
/// - `[email protected]` (FIDO/U2F key)
///
/// Any other algorithms are mapped to the [`Algorithm::Other`] variant.
pub fn new(id: &str) -> Result<Self> {
Ok(id.parse()?)
}
Expand All @@ -147,6 +159,8 @@ impl Algorithm {
/// - `[email protected]` (FIDO/U2F key)
/// - `[email protected]` (FIDO/U2F key)
///
/// Any other algorithms are mapped to the [`Algorithm::Other`] variant.
///
/// [PROTOCOL.certkeys]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
pub fn new_certificate(id: &str) -> Result<Self> {
match id {
Expand All @@ -164,12 +178,15 @@ impl Algorithm {
CERT_RSA => Ok(Algorithm::Rsa { hash: None }),
CERT_SK_ECDSA_SHA2_P256 => Ok(Algorithm::SkEcdsaSha2NistP256),
CERT_SK_SSH_ED25519 => Ok(Algorithm::SkEd25519),
#[cfg(feature = "alloc")]
_ => Ok(Algorithm::Other(AlgorithmName::from_certificate_str(id)?)),
#[cfg(not(feature = "alloc"))]
_ => Err(Error::AlgorithmUnknown),
}
}

/// Get the string identifier which corresponds to this algorithm.
pub fn as_str(self) -> &'static str {
pub fn as_str(&self) -> &str {
match self {
Algorithm::Dsa => SSH_DSA,
Algorithm::Ecdsa { curve } => match curve {
Expand All @@ -185,6 +202,8 @@ impl Algorithm {
},
Algorithm::SkEcdsaSha2NistP256 => SK_ECDSA_SHA2_P256,
Algorithm::SkEd25519 => SK_SSH_ED25519,
#[cfg(feature = "alloc")]
Algorithm::Other(algorithm) => algorithm.as_str(),
}
}

Expand All @@ -195,7 +214,7 @@ impl Algorithm {
/// See [PROTOCOL.certkeys] for more information.
///
/// [PROTOCOL.certkeys]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
pub fn as_certificate_str(self) -> &'static str {
pub fn as_certificate_str(&self) -> &str {
match self {
Algorithm::Dsa => CERT_DSA,
Algorithm::Ecdsa { curve } => match curve {
Expand All @@ -207,6 +226,8 @@ impl Algorithm {
Algorithm::Rsa { .. } => CERT_RSA,
Algorithm::SkEcdsaSha2NistP256 => CERT_SK_ECDSA_SHA2_P256,
Algorithm::SkEd25519 => CERT_SK_SSH_ED25519,
#[cfg(feature = "alloc")]
Algorithm::Other(algorithm) => algorithm.certificate_str(),
}
}

Expand Down Expand Up @@ -276,6 +297,9 @@ impl str::FromStr for Algorithm {
SSH_RSA => Ok(Algorithm::Rsa { hash: None }),
SK_ECDSA_SHA2_P256 => Ok(Algorithm::SkEcdsaSha2NistP256),
SK_SSH_ED25519 => Ok(Algorithm::SkEd25519),
#[cfg(feature = "alloc")]
_ => Ok(Algorithm::Other(AlgorithmName::from_str(id)?)),
#[cfg(not(feature = "alloc"))]
_ => Err(LabelError::new(id)),
}
}
Expand Down
109 changes: 109 additions & 0 deletions ssh-key/src/algorithm/name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use alloc::string::String;
use core::str::{self, FromStr};
use encoding::LabelError;

/// The suffix added to the `name` in a `name@domainname` algorithm string identifier.
const CERT_STR_SUFFIX: &str = "-cert-v01";

/// According to [RFC4251 § 6], algorithm names are ASCII strings that are at most 64
/// characters long.
///
/// [RFC4251 § 6]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6
const MAX_ALGORITHM_NAME_LEN: usize = 64;

/// The maximum length of the certificate string identifier is [`MAX_ALGORITHM_NAME_LEN`] +
/// `"-cert-v01".len()` (the certificate identifier is obtained by inserting `"-cert-v01"` in the
/// algorithm name).
const MAX_CERT_STR_LEN: usize = MAX_ALGORITHM_NAME_LEN + CERT_STR_SUFFIX.len();

/// A string representing an additional algorithm name in the `name@domainname` format (see
/// [RFC4251 § 6]).
///
/// Additional algorithm names must be non-empty printable ASCII strings no longer than 64
/// characters.
///
/// This also provides a `name-cert-v01@domainnname` string identifier for the corresponding
/// OpenSSH certificate format, derived from the specified `name@domainname` string.
///
/// NOTE: RFC4251 specifies additional validation criteria for algorithm names, but we do not
/// implement all of them here.
///
/// [RFC4251 § 6]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct AlgorithmName {
/// The string identifier which corresponds to this algorithm.
id: String,
/// The string identifier which corresponds to the OpenSSH certificate format.
///
/// This is derived from the algorithm name by inserting `"-cert-v01"` immediately after the
/// name preceding the at-symbol (`@`).
certificate_str: String,
}

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

/// Get the string identifier which corresponds to the OpenSSH certificate format.
pub fn certificate_str(&self) -> &str {
&self.certificate_str
}

/// Create a new [`AlgorithmName`] from an OpenSSH certificate format string identifier.
pub fn from_certificate_str(id: &str) -> Result<Self, LabelError> {
validate_algorithm_id(id, MAX_CERT_STR_LEN)?;

// Derive the algorithm name from the certificate format string identifier:
let (name, domain) = split_algorithm_id(id)?;
let name = name
.strip_suffix(CERT_STR_SUFFIX)
.ok_or_else(|| LabelError::new(id))?;

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

Ok(Self {
id: algorithm_name,
certificate_str: id.into(),
})
}
}

impl FromStr for AlgorithmName {
type Err = LabelError;

fn from_str(id: &str) -> Result<Self, LabelError> {
validate_algorithm_id(id, MAX_ALGORITHM_NAME_LEN)?;

// Derive the certificate format string identifier from the algorithm name:
let (name, domain) = split_algorithm_id(id)?;
let certificate_str = format!("{name}{CERT_STR_SUFFIX}@{domain}");

Ok(Self {
id: id.into(),
certificate_str,
})
}
}

/// Check if the length of `id` is at most `n`, and that `id` only consists of ASCII characters.
fn validate_algorithm_id(id: &str, n: usize) -> Result<(), LabelError> {
if id.len() > n || !id.is_ascii() {
return Err(LabelError::new(id));
}

Ok(())
}

/// Split a `name@domainname` algorithm string identifier into `(name, domainname)`.
fn split_algorithm_id(id: &str) -> Result<(&str, &str), LabelError> {
let (name, domain) = id.split_once('@').ok_or_else(|| LabelError::new(id))?;

// TODO: validate name and domain_name according to the criteria from RFC4251
if name.is_empty() || domain.is_empty() || domain.contains('@') {
return Err(LabelError::new(id));
}

Ok((name, domain))
}
1 change: 1 addition & 0 deletions ssh-key/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ pub use sha2;

#[cfg(feature = "alloc")]
pub use crate::{
algorithm::AlgorithmName,
certificate::Certificate,
known_hosts::KnownHosts,
mpint::Mpint,
Expand Down
3 changes: 3 additions & 0 deletions ssh-key/src/private.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ mod ecdsa;
mod ed25519;
mod keypair;
#[cfg(feature = "alloc")]
mod opaque;
#[cfg(feature = "alloc")]
mod rsa;
#[cfg(feature = "alloc")]
mod sk;
Expand All @@ -124,6 +126,7 @@ pub use self::{
pub use crate::{
private::{
dsa::{DsaKeypair, DsaPrivateKey},
opaque::{OpaqueKeypair, OpaqueKeypairBytes, OpaquePrivateKeyBytes},
rsa::{RsaKeypair, RsaPrivateKey},
sk::SkEd25519,
},
Expand Down
37 changes: 36 additions & 1 deletion ssh-key/src/private/keypair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use subtle::{Choice, ConstantTimeEq};

#[cfg(feature = "alloc")]
use {
super::{DsaKeypair, RsaKeypair, SkEd25519},
super::{DsaKeypair, OpaqueKeypair, RsaKeypair, SkEd25519},
alloc::vec::Vec,
};

Expand Down Expand Up @@ -55,6 +55,10 @@ pub enum KeypairData {
/// [PROTOCOL.u2f]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.u2f?annotate=HEAD
#[cfg(feature = "alloc")]
SkEd25519(SkEd25519),

/// Opaque keypair.
#[cfg(feature = "alloc")]
Other(OpaqueKeypair),
}

impl KeypairData {
Expand All @@ -74,6 +78,8 @@ impl KeypairData {
Self::SkEcdsaSha2NistP256(_) => Algorithm::SkEcdsaSha2NistP256,
#[cfg(feature = "alloc")]
Self::SkEd25519(_) => Algorithm::SkEd25519,
#[cfg(feature = "alloc")]
Self::Other(key) => key.algorithm(),
})
}

Expand Down Expand Up @@ -140,6 +146,15 @@ impl KeypairData {
}
}

/// Get the custom, opaque private key if this key is the correct type.
#[cfg(feature = "alloc")]
pub fn other(&self) -> Option<&OpaqueKeypair> {
match self {
Self::Other(key) => Some(key),
_ => None,
}
}

/// Is this key a DSA key?
#[cfg(feature = "alloc")]
pub fn is_dsa(&self) -> bool {
Expand Down Expand Up @@ -187,6 +202,12 @@ impl KeypairData {
matches!(self, Self::SkEd25519(_))
}

/// Is this a key with a custom algorithm?
#[cfg(feature = "alloc")]
pub fn is_other(&self) -> bool {
matches!(self, Self::Other(_))
}

/// Compute a deterministic "checkint" for this private key.
///
/// This is a sort of primitive pseudo-MAC used by the OpenSSH key format.
Expand All @@ -206,6 +227,8 @@ impl KeypairData {
Self::SkEcdsaSha2NistP256(sk) => sk.key_handle(),
#[cfg(feature = "alloc")]
Self::SkEd25519(sk) => sk.key_handle(),
#[cfg(feature = "alloc")]
Self::Other(key) => key.private.as_ref(),
};

let mut n = 0u32;
Expand Down Expand Up @@ -243,6 +266,8 @@ impl ConstantTimeEq for KeypairData {
// The key structs contain all public data.
Choice::from((a == b) as u8)
}
#[cfg(feature = "alloc")]
(Self::Other(a), Self::Other(b)) => a.ct_eq(b),
#[allow(unreachable_patterns)]
_ => Choice::from(0),
}
Expand Down Expand Up @@ -278,6 +303,10 @@ impl Decode for KeypairData {
}
#[cfg(feature = "alloc")]
Algorithm::SkEd25519 => SkEd25519::decode(reader).map(Self::SkEd25519),
#[cfg(feature = "alloc")]
algorithm @ Algorithm::Other(_) => {
OpaqueKeypair::decode_as(reader, algorithm).map(Self::Other)
}
#[allow(unreachable_patterns)]
_ => Err(Error::AlgorithmUnknown),
}
Expand Down Expand Up @@ -307,6 +336,8 @@ impl Encode for KeypairData {
Self::SkEcdsaSha2NistP256(sk) => sk.encoded_len()?,
#[cfg(feature = "alloc")]
Self::SkEd25519(sk) => sk.encoded_len()?,
#[cfg(feature = "alloc")]
Self::Other(key) => key.encoded_len()?,
};

[alg_len, key_len].checked_sum()
Expand All @@ -331,6 +362,8 @@ impl Encode for KeypairData {
Self::SkEcdsaSha2NistP256(sk) => sk.encode(writer)?,
#[cfg(feature = "alloc")]
Self::SkEd25519(sk) => sk.encode(writer)?,
#[cfg(feature = "alloc")]
Self::Other(key) => key.encode(writer)?,
}

Ok(())
Expand All @@ -357,6 +390,8 @@ impl TryFrom<&KeypairData> for public::KeyData {
}
#[cfg(feature = "alloc")]
KeypairData::SkEd25519(sk) => public::KeyData::SkEd25519(sk.public().clone()),
#[cfg(feature = "alloc")]
KeypairData::Other(key) => public::KeyData::Other(key.into()),
})
}
}
Expand Down
Loading

0 comments on commit fef4dca

Please sign in to comment.