From 141291e1875b55e0f9e45114f73f99eb16270b0e Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Fri, 7 Jul 2023 16:24:13 +0100 Subject: [PATCH 01/10] ssh-key: define a type for custom algorithm names. Adds an `AlgorithmName` type for additional algorithm names. The syntax for additional algorithm names is described in [section 6 of RFC4251]. Introduces a dependency on `tinystr`. Using `tinystr::TinyAsciiStr` for the representing algorithm names enables `AlgorithmName` to be `Copy`. [section 6 of RFC4261]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6 --- Cargo.lock | 21 ++++++ ssh-key/Cargo.toml | 4 +- ssh-key/src/algorithm.rs | 116 ++++++++++++++++++++++++++++++++ ssh-key/src/lib.rs | 1 + ssh-key/tests/algorithm_name.rs | 56 +++++++++++++++ 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 ssh-key/tests/algorithm_name.rs diff --git a/Cargo.lock b/Cargo.lock index ebdbcdf5..1179718a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,6 +240,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dsa" version = "0.6.1" @@ -792,6 +803,7 @@ dependencies = [ "ssh-cipher", "ssh-encoding", "subtle", + "tinystr", "zeroize", ] @@ -812,6 +824,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tinystr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ac3f5b6856e931e15e07b478e98c8045239829a65f9156d4fa7e7788197a5ef" +dependencies = [ + "displaydoc", +] + [[package]] name = "typenum" version = "1.16.0" diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index e1814b18..2e35341f 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -36,6 +36,7 @@ rsa = { version = "0.9", optional = true, default-features = false, features = [ sec1 = { version = "0.7", optional = true, default-features = false, features = ["point"] } serde = { version = "1", optional = true } sha1 = { version = "0.10", optional = true, default-features = false } +tinystr = { version = "0.7.1", optional = true, default-features = false } [dev-dependencies] hex-literal = "0.4.1" @@ -46,7 +47,8 @@ default = ["ecdsa", "rand_core", "std"] alloc = [ "encoding/alloc", "signature/alloc", - "zeroize/alloc" + "zeroize/alloc", + "dep:tinystr" ] std = [ "alloc", diff --git a/ssh-key/src/algorithm.rs b/ssh-key/src/algorithm.rs index 8f497cc4..d9462ad9 100644 --- a/ssh-key/src/algorithm.rs +++ b/ssh-key/src/algorithm.rs @@ -8,6 +8,7 @@ use encoding::{Label, LabelError}; use { alloc::vec::Vec, sha2::{Digest, Sha256, Sha512}, + tinystr::TinyAsciiStr, }; /// bcrypt-pbkdf @@ -76,6 +77,121 @@ const SK_ECDSA_SHA2_P256: &str = "sk-ecdsa-sha2-nistp256@openssh.com"; /// U2F/FIDO security key with Ed25519 const SK_SSH_ED25519: &str = "sk-ssh-ed25519@openssh.com"; +/// The suffix added to the `name` in a `name@domainname` algorithm string identifier. +#[cfg(feature = "alloc")] +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 +#[cfg(feature = "alloc")] +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). +#[cfg(feature = "alloc")] +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 +// +// NOTE: We use TinyAsciiStr instead of String to allow Algorithm to implement Copy. +#[cfg(feature = "alloc")] +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct AlgorithmName { + /// The string identifier which corresponds to this algorithm. + id: TinyAsciiStr, + /// 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: TinyAsciiStr, +} + +#[cfg(feature = "alloc")] +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) -> core::result::Result { + if id.len() > MAX_CERT_STR_LEN { + return Err(LabelError::new(id)); + } + + let certificate_str = TinyAsciiStr::from_str(id).map_err(|_| LabelError::new(id))?; + + // Derive the algorithm name from the certificate format string identifier: + 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)); + } + + let name = name + .strip_suffix(CERT_STR_SUFFIX) + .ok_or_else(|| LabelError::new(id))?; + + let algorithm_name = + TinyAsciiStr::from_str(&format!("{name}@{domain}")).map_err(|_| LabelError::new(id))?; + + Ok(Self { + id: algorithm_name, + certificate_str, + }) + } +} + +#[cfg(feature = "alloc")] +impl str::FromStr for AlgorithmName { + type Err = LabelError; + + fn from_str(id: &str) -> core::result::Result { + if id.len() > MAX_ALGORITHM_NAME_LEN { + return Err(LabelError::new(id)); + } + + let algorithm_name = TinyAsciiStr::from_str(id).map_err(|_| LabelError::new(id))?; + + // Derive the certificate format string identifier from the algorithm name: + 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)); + } + + let certificate_str = TinyAsciiStr::from_str(&format!("{name}{CERT_STR_SUFFIX}@{domain}")) + .map_err(|_| LabelError::new(id))?; + + Ok(Self { + id: algorithm_name, + certificate_str, + }) + } +} + /// SSH key algorithms. /// /// This type provides a registry of supported digital signature algorithms diff --git a/ssh-key/src/lib.rs b/ssh-key/src/lib.rs index 424030e1..278730aa 100644 --- a/ssh-key/src/lib.rs +++ b/ssh-key/src/lib.rs @@ -174,6 +174,7 @@ pub use sha2; #[cfg(feature = "alloc")] pub use crate::{ + algorithm::AlgorithmName, certificate::Certificate, known_hosts::KnownHosts, mpint::Mpint, diff --git a/ssh-key/tests/algorithm_name.rs b/ssh-key/tests/algorithm_name.rs new file mode 100644 index 00000000..3ff5ccd3 --- /dev/null +++ b/ssh-key/tests/algorithm_name.rs @@ -0,0 +1,56 @@ +//! Tests for `AlgorithmName` parsing. + +#![cfg(feature = "alloc")] + +use ssh_key::AlgorithmName; +use std::str::FromStr; + +#[test] +fn additional_algorithm_name() { + const NAME: &str = "name@example.com"; + const CERT_STR: &str = "name-cert-v01@example.com"; + + let name = AlgorithmName::from_str(NAME).unwrap(); + assert_eq!(name.as_str(), NAME); + assert_eq!(name.certificate_str(), CERT_STR); + + let name = AlgorithmName::from_certificate_str(CERT_STR).unwrap(); + assert_eq!(name.as_str(), NAME); + assert_eq!(name.certificate_str(), CERT_STR); +} + +#[test] +fn invalid_algorithm_name() { + const INVALID_NAMES: &[&str] = &[ + "nameß@example.com", + "name@example@com", + "name", + "@name", + "name@", + "", + "@", + "a-name-that-is-too-long-but-would-otherwise-be-valid-@example.com", + ]; + + const INVALID_CERT_STRS: &[&str] = &[ + "nameß-cert-v01@example.com", + "name-cert-v01@example@com", + "name@example.com", + ]; + + for name in INVALID_NAMES { + assert!( + AlgorithmName::from_str(&name).is_err(), + "{:?} should be an invalid algorithm name", + name + ); + } + + for name in INVALID_CERT_STRS { + assert!( + AlgorithmName::from_certificate_str(&name).is_err(), + "{:?} should be an invalid certificate str", + name + ); + } +} From 5896d12efe94d609a4c29233d55f214ffe5f88fc Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Sat, 8 Jul 2023 21:18:26 +0100 Subject: [PATCH 02/10] ssh-key: add a catch-all variant to Algorithm. 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`. --- ssh-key/src/algorithm.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/ssh-key/src/algorithm.rs b/ssh-key/src/algorithm.rs index d9462ad9..356064b6 100644 --- a/ssh-key/src/algorithm.rs +++ b/ssh-key/src/algorithm.rs @@ -229,6 +229,10 @@ pub enum Algorithm { /// FIDO/U2F key with Ed25519 SkEd25519, + + /// Other + #[cfg(feature = "alloc")] + Other(AlgorithmName), } impl Algorithm { @@ -243,6 +247,8 @@ impl Algorithm { /// - `ssh-rsa` /// - `sk-ecdsa-sha2-nistp256@openssh.com` (FIDO/U2F key) /// - `sk-ssh-ed25519@openssh.com` (FIDO/U2F key) + /// + /// Any other algorithms are mapped to the [`Algorithm::Other`] variant. pub fn new(id: &str) -> Result { Ok(id.parse()?) } @@ -263,6 +269,8 @@ impl Algorithm { /// - `sk-ecdsa-sha2-nistp256-cert-v01@openssh.com` (FIDO/U2F key) /// - `sk-ssh-ed25519-cert-v01@openssh.com` (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 { match id { @@ -280,12 +288,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 { @@ -301,6 +312,8 @@ impl Algorithm { }, Algorithm::SkEcdsaSha2NistP256 => SK_ECDSA_SHA2_P256, Algorithm::SkEd25519 => SK_SSH_ED25519, + #[cfg(feature = "alloc")] + Algorithm::Other(algorithm) => algorithm.as_str(), } } @@ -311,7 +324,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 { @@ -323,6 +336,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(), } } @@ -392,6 +407,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)), } } From ee4e023e12183a01668839f6269b88f62fced482 Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Sat, 8 Jul 2023 21:19:09 +0100 Subject: [PATCH 03/10] ssh-key: create `Keypair` and `KeyData` variants for custom algorithms. 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. --- ssh-key/src/private.rs | 3 + ssh-key/src/private/keypair.rs | 37 ++++++- ssh-key/src/private/opaque.rs | 155 +++++++++++++++++++++++++++ ssh-key/src/public.rs | 8 +- ssh-key/src/public/key_data.rs | 29 ++++- ssh-key/src/public/opaque.rs | 98 +++++++++++++++++ ssh-key/tests/examples/id_opaque | 7 ++ ssh-key/tests/examples/id_opaque.pub | 1 + ssh-key/tests/private_key.rs | 38 ++++++- ssh-key/tests/public_key.rs | 26 +++++ 10 files changed, 398 insertions(+), 4 deletions(-) create mode 100644 ssh-key/src/private/opaque.rs create mode 100644 ssh-key/src/public/opaque.rs create mode 100644 ssh-key/tests/examples/id_opaque create mode 100644 ssh-key/tests/examples/id_opaque.pub diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index ed32c8b0..5e5ad8d0 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -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; @@ -124,6 +126,7 @@ pub use self::{ pub use crate::{ private::{ dsa::{DsaKeypair, DsaPrivateKey}, + opaque::{OpaqueKeypair, OpaqueKeypairBytes, OpaquePrivateKeyBytes}, rsa::{RsaKeypair, RsaPrivateKey}, sk::SkEd25519, }, diff --git a/ssh-key/src/private/keypair.rs b/ssh-key/src/private/keypair.rs index f8dc992b..e21daa19 100644 --- a/ssh-key/src/private/keypair.rs +++ b/ssh-key/src/private/keypair.rs @@ -7,7 +7,7 @@ use subtle::{Choice, ConstantTimeEq}; #[cfg(feature = "alloc")] use { - super::{DsaKeypair, RsaKeypair, SkEd25519}, + super::{DsaKeypair, OpaqueKeypair, RsaKeypair, SkEd25519}, alloc::vec::Vec, }; @@ -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 { @@ -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(), }) } @@ -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 { @@ -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. @@ -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; @@ -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), } @@ -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), } @@ -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() @@ -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(()) @@ -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()), }) } } diff --git a/ssh-key/src/private/opaque.rs b/ssh-key/src/private/opaque.rs new file mode 100644 index 00000000..00cc5698 --- /dev/null +++ b/ssh-key/src/private/opaque.rs @@ -0,0 +1,155 @@ +//! Opaque private keys. +//! +//! [`OpaqueKeypair`] represents a keypair meant to be used with an algorithm unknown to this +//! crate, i.e. keypairs that use a custom algorithm as specified in [RFC4251 § 6]. +//! +//! They are said to be opaque, because the meaning of their underlying byte representation is not +//! specified. +//! +//! [RFC4251 § 6]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6 + +use crate::{ + public::{OpaquePublicKey, OpaquePublicKeyBytes}, + Algorithm, Error, Result, +}; +use alloc::vec::Vec; +use core::fmt; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; +use subtle::{Choice, ConstantTimeEq}; + +/// An opaque private key. +/// +/// The encoded representation of an `OpaquePrivateKeyBytes` consists of a 4-byte length prefix, +/// followed by its byte representation. +#[derive(Clone)] +pub struct OpaquePrivateKeyBytes(Vec); + +/// An opaque keypair. +/// +/// The encoded representation of an `OpaqueKeypair` consists of the encoded representation of its +/// [`OpaquePublicKey`] followed by the encoded representation of its [`OpaquePrivateKeyBytes`]. +#[derive(Clone)] +pub struct OpaqueKeypair { + /// The opaque private key + pub private: OpaquePrivateKeyBytes, + /// The opaque public key + pub public: OpaquePublicKey, +} + +/// The underlying representation of an [`OpaqueKeypair`]. +/// +/// The encoded representation of an `OpaqueKeypairBytes` consists of the encoded representation of +/// its [`OpaquePublicKeyBytes`] followed by the encoded representation of its +/// [`OpaquePrivateKeyBytes`]. +pub struct OpaqueKeypairBytes { + /// The opaque private key + pub private: OpaquePrivateKeyBytes, + /// The opaque public key + pub public: OpaquePublicKeyBytes, +} + +impl OpaqueKeypair { + /// Create a new `OpaqueKeypair`. + pub fn new(private_key: Vec, public: OpaquePublicKey) -> Self { + Self { + private: OpaquePrivateKeyBytes(private_key), + public, + } + } + + /// Get the [`Algorithm`] for this key type. + pub fn algorithm(&self) -> Algorithm { + self.public.algorithm() + } + + /// Decode [`OpaqueKeypair`] for the specified algorithm. + pub(super) fn decode_as(reader: &mut impl Reader, algorithm: Algorithm) -> Result { + let key = OpaqueKeypairBytes::decode(reader)?; + let public = OpaquePublicKey { + algorithm, + key: key.public, + }; + + Ok(Self { + public, + private: key.private, + }) + } +} + +impl Decode for OpaquePrivateKeyBytes { + type Error = Error; + + fn decode(reader: &mut impl Reader) -> Result { + let len = usize::decode(reader)?; + let mut bytes = vec![0; len]; + reader.read(&mut bytes)?; + Ok(Self(bytes)) + } +} + +impl Decode for OpaqueKeypairBytes { + type Error = Error; + + fn decode(reader: &mut impl Reader) -> Result { + let public = OpaquePublicKeyBytes::decode(reader)?; + let private = OpaquePrivateKeyBytes::decode(reader)?; + + Ok(Self { public, private }) + } +} + +impl Encode for OpaqueKeypair { + fn encoded_len(&self) -> encoding::Result { + [self.public.encoded_len()?, self.private.encoded_len()?].checked_sum() + } + + fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> { + self.public.encode(writer)?; + self.private.encode(writer)?; + + Ok(()) + } +} + +impl ConstantTimeEq for OpaqueKeypair { + fn ct_eq(&self, other: &Self) -> Choice { + Choice::from((self.public == other.public) as u8) & self.private.ct_eq(&other.private) + } +} + +impl Encode for OpaquePrivateKeyBytes { + fn encoded_len(&self) -> encoding::Result { + self.0.encoded_len() + } + + fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> { + self.0.encode(writer) + } +} + +impl From<&OpaqueKeypair> for OpaquePublicKey { + fn from(keypair: &OpaqueKeypair) -> OpaquePublicKey { + keypair.public.clone() + } +} + +impl fmt::Debug for OpaqueKeypair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OpaqueKeypair") + .field("public", &self.public) + .finish_non_exhaustive() + } +} + +impl ConstantTimeEq for OpaquePrivateKeyBytes { + fn ct_eq(&self, other: &Self) -> Choice { + self.as_ref().ct_eq(other.as_ref()) + } +} + +impl AsRef<[u8]> for OpaquePrivateKeyBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/ssh-key/src/public.rs b/ssh-key/src/public.rs index 64725eaf..50d95821 100644 --- a/ssh-key/src/public.rs +++ b/ssh-key/src/public.rs @@ -9,6 +9,8 @@ mod ecdsa; mod ed25519; mod key_data; #[cfg(feature = "alloc")] +mod opaque; +#[cfg(feature = "alloc")] mod rsa; mod sk; mod ssh_format; @@ -16,7 +18,11 @@ mod ssh_format; pub use self::{ed25519::Ed25519PublicKey, key_data::KeyData, sk::SkEd25519}; #[cfg(feature = "alloc")] -pub use self::{dsa::DsaPublicKey, rsa::RsaPublicKey}; +pub use self::{ + dsa::DsaPublicKey, + opaque::{OpaquePublicKey, OpaquePublicKeyBytes}, + rsa::RsaPublicKey, +}; #[cfg(feature = "ecdsa")] pub use self::{ecdsa::EcdsaPublicKey, sk::SkEcdsaSha2NistP256}; diff --git a/ssh-key/src/public/key_data.rs b/ssh-key/src/public/key_data.rs index 1fd7a77d..83b1510f 100644 --- a/ssh-key/src/public/key_data.rs +++ b/ssh-key/src/public/key_data.rs @@ -5,7 +5,7 @@ use crate::{Algorithm, Error, Fingerprint, HashAlg, Result}; use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; #[cfg(feature = "alloc")] -use super::{DsaPublicKey, RsaPublicKey}; +use super::{DsaPublicKey, OpaquePublicKey, RsaPublicKey}; #[cfg(feature = "ecdsa")] use super::{EcdsaPublicKey, SkEcdsaSha2NistP256}; @@ -39,6 +39,10 @@ pub enum KeyData { /// /// [PROTOCOL.u2f]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.u2f?annotate=HEAD SkEd25519(SkEd25519), + + /// Opaque public key data. + #[cfg(feature = "alloc")] + Other(OpaquePublicKey), } impl KeyData { @@ -55,6 +59,8 @@ impl KeyData { #[cfg(feature = "ecdsa")] Self::SkEcdsaSha2NistP256(_) => Algorithm::SkEcdsaSha2NistP256, Self::SkEd25519(_) => Algorithm::SkEd25519, + #[cfg(feature = "alloc")] + Self::Other(key) => key.algorithm(), } } @@ -118,6 +124,15 @@ impl KeyData { } } + /// Get the custom, opaque public key if this key is the correct type. + #[cfg(feature = "alloc")] + pub fn other(&self) -> Option<&OpaquePublicKey> { + match self { + Self::Other(key) => Some(key), + _ => None, + } + } + /// Is this key a DSA key? #[cfg(feature = "alloc")] pub fn is_dsa(&self) -> bool { @@ -152,6 +167,12 @@ impl KeyData { 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(_)) + } + /// Decode [`KeyData`] for the specified algorithm. pub(crate) fn decode_as(reader: &mut impl Reader, algorithm: Algorithm) -> Result { match algorithm { @@ -170,6 +191,8 @@ impl KeyData { SkEcdsaSha2NistP256::decode(reader).map(Self::SkEcdsaSha2NistP256) } Algorithm::SkEd25519 => SkEd25519::decode(reader).map(Self::SkEd25519), + #[cfg(feature = "alloc")] + Algorithm::Other(_) => OpaquePublicKey::decode_as(reader, algorithm).map(Self::Other), #[allow(unreachable_patterns)] _ => Err(Error::AlgorithmUnknown), } @@ -189,6 +212,8 @@ impl KeyData { #[cfg(feature = "ecdsa")] Self::SkEcdsaSha2NistP256(sk) => sk.encoded_len(), Self::SkEd25519(sk) => sk.encoded_len(), + #[cfg(feature = "alloc")] + Self::Other(other) => other.key.encoded_len(), } } @@ -205,6 +230,8 @@ impl KeyData { #[cfg(feature = "ecdsa")] Self::SkEcdsaSha2NistP256(sk) => sk.encode(writer), Self::SkEd25519(sk) => sk.encode(writer), + #[cfg(feature = "alloc")] + Self::Other(other) => other.key.encode(writer), } } } diff --git a/ssh-key/src/public/opaque.rs b/ssh-key/src/public/opaque.rs new file mode 100644 index 00000000..2526646e --- /dev/null +++ b/ssh-key/src/public/opaque.rs @@ -0,0 +1,98 @@ +//! Opaque public keys. +//! +//! [`OpaquePublicKey`] represents a public key meant to be used with an algorithm unknown to this +//! crate, i.e. public keys that use a custom algorithm as specified in [RFC4251 § 6]. +//! +//! They are said to be opaque, because the meaning of their underlying byte representation is not +//! specified. +//! +//! [RFC4251 § 6]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6 + +use crate::{Algorithm, Error, Result}; +use alloc::vec::Vec; +use encoding::{Decode, Encode, Reader, Writer}; + +/// An opaque public key with a custom algorithm name. +/// +/// The encoded representation of an `OpaquePublicKey` is the encoded representation of its +/// [`OpaquePublicKeyBytes`]. +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct OpaquePublicKey { + /// The [`Algorithm`] of this public key. + pub algorithm: Algorithm, + /// The key data + pub key: OpaquePublicKeyBytes, +} + +/// The underlying representation of an [`OpaquePublicKey`]. +/// +/// The encoded representation of an `OpaquePublicKeyBytes` consists of a 4-byte length prefix, +/// followed by its byte representation. +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct OpaquePublicKeyBytes(Vec); + +impl OpaquePublicKey { + /// Create a new `OpaquePublicKey`. + pub fn new(key: Vec, algorithm: Algorithm) -> Self { + Self { + key: OpaquePublicKeyBytes(key), + algorithm, + } + } + + /// Get the [`Algorithm`] for this public key type. + pub fn algorithm(&self) -> Algorithm { + self.algorithm + } + + /// Decode [`OpaquePublicKey`] for the specified algorithm. + pub(super) fn decode_as(reader: &mut impl Reader, algorithm: Algorithm) -> Result { + Ok(Self { + algorithm, + key: OpaquePublicKeyBytes::decode(reader)?, + }) + } +} + +impl Decode for OpaquePublicKeyBytes { + type Error = Error; + + fn decode(reader: &mut impl Reader) -> Result { + let len = usize::decode(reader)?; + let mut bytes = vec![0; len]; + reader.read(&mut bytes)?; + Ok(Self(bytes)) + } +} + +impl Encode for OpaquePublicKeyBytes { + fn encoded_len(&self) -> encoding::Result { + self.0.encoded_len() + } + + fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> { + self.0.encode(writer) + } +} + +impl Encode for OpaquePublicKey { + fn encoded_len(&self) -> encoding::Result { + self.key.encoded_len() + } + + fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> { + self.key.encode(writer) + } +} + +impl AsRef<[u8]> for OpaquePublicKey { + fn as_ref(&self) -> &[u8] { + self.key.as_ref() + } +} + +impl AsRef<[u8]> for OpaquePublicKeyBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/ssh-key/tests/examples/id_opaque b/ssh-key/tests/examples/id_opaque new file mode 100644 index 00000000..3c5abfd3 --- /dev/null +++ b/ssh-key/tests/examples/id_opaque @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAOAAAABBuYW1lQG +V4YW1wbGUuY29tAAAAIIiPJO4Xrf7QCR5n5IX7mETP7WByysHQY5DkAF9QFbRPAAAAgH7Q +MVF+0DFRAAAAEG5hbWVAZXhhbXBsZS5jb20AAAAgiI8k7het/tAJHmfkhfuYRM/tYHLKwd +BjkOQAX1AVtE8AAAAgmGyVO0te+zKF/yB8HKXuOaWQR7xIj7w7HvA278dXXHUAAAATY29t +bWVudEBleGFtcGxlLmNvbQECAwQF +-----END OPENSSH PRIVATE KEY----- diff --git a/ssh-key/tests/examples/id_opaque.pub b/ssh-key/tests/examples/id_opaque.pub new file mode 100644 index 00000000..0a0c1484 --- /dev/null +++ b/ssh-key/tests/examples/id_opaque.pub @@ -0,0 +1 @@ +name@example.com AAAAEG5hbWVAZXhhbXBsZS5jb20AAAAgiI8k7het/tAJHmfkhfuYRM/tYHLKwdBjkOQAX1AVtE8= comment@example.com diff --git a/ssh-key/tests/private_key.rs b/ssh-key/tests/private_key.rs index 17a59ec7..d1d78b74 100644 --- a/ssh-key/tests/private_key.rs +++ b/ssh-key/tests/private_key.rs @@ -42,6 +42,10 @@ const OPENSSH_RSA_3072_EXAMPLE: &str = include_str!("examples/id_rsa_3072"); #[cfg(feature = "alloc")] const OPENSSH_RSA_4096_EXAMPLE: &str = include_str!("examples/id_rsa_4096"); +/// OpenSSH-formatted private key with a custom algorithm name +#[cfg(feature = "alloc")] +const OPENSSH_OPAQUE_EXAMPLE: &str = include_str!("examples/id_opaque"); + #[cfg(feature = "alloc")] #[test] fn decode_dsa_openssh() { @@ -367,6 +371,30 @@ fn decode_rsa_4096_openssh() { assert_eq!("user@example.com", key.comment()); } +#[cfg(all(feature = "alloc"))] +#[test] +fn decode_custom_algorithm_openssh() { + let key = PrivateKey::from_openssh(OPENSSH_OPAQUE_EXAMPLE).unwrap(); + assert!( + matches!(key.algorithm(), Algorithm::Other(name) if name.as_str() == "name@example.com") + ); + assert_eq!(Cipher::None, key.cipher()); + assert_eq!(KdfAlg::None, key.kdf().algorithm()); + assert!(key.kdf().is_none()); + + let opaque_keypair = key.key_data().other().unwrap(); + assert_eq!( + &hex!("888f24ee17adfed0091e67e485fb9844cfed6072cac1d06390e4005f5015b44f"), + opaque_keypair.public.as_ref(), + ); + assert_eq!( + &hex!("986c953b4b5efb3285ff207c1ca5ee39a59047bc488fbc3b1ef036efc7575c75"), + opaque_keypair.private.as_ref(), + ); + + assert_eq!(key.comment(), "comment@example.com"); +} + #[cfg(all(feature = "alloc"))] #[test] fn encode_dsa_openssh() { @@ -409,6 +437,12 @@ fn encode_rsa_4096_openssh() { encoding_test(OPENSSH_RSA_4096_EXAMPLE) } +#[cfg(all(feature = "alloc"))] +#[test] +fn encode_custom_algorithm_openssh() { + encoding_test(OPENSSH_OPAQUE_EXAMPLE) +} + /// Common behavior of all encoding tests #[cfg(all(feature = "alloc"))] fn encoding_test(private_key: &str) { @@ -420,7 +454,9 @@ fn encoding_test(private_key: &str) { assert_eq!(key, key2); #[cfg(feature = "std")] - encoding_integration_test(key) + if !matches!(key.algorithm(), Algorithm::Other(_)) { + encoding_integration_test(key) + } } /// Parse PEM encoded using `PrivateKey::to_openssh` using the `ssh-keygen` utility. diff --git a/ssh-key/tests/public_key.rs b/ssh-key/tests/public_key.rs index 0f78f82c..05e235f6 100644 --- a/ssh-key/tests/public_key.rs +++ b/ssh-key/tests/public_key.rs @@ -40,6 +40,10 @@ const OPENSSH_SK_ECDSA_P256_EXAMPLE: &str = include_str!("examples/id_sk_ecdsa_p /// Security Key (FIDO/U2F) Ed25519 OpenSSH-formatted public key const OPENSSH_SK_ED25519_EXAMPLE: &str = include_str!("examples/id_sk_ed25519.pub"); +/// OpenSSH-formatted public key with a custom algorithm name +#[cfg(feature = "alloc")] +const OPENSSH_OPAQUE_EXAMPLE: &str = include_str!("examples/id_opaque.pub"); + #[cfg(feature = "alloc")] #[test] fn decode_dsa_openssh() { @@ -304,6 +308,28 @@ fn decode_sk_ed25519_openssh() { ); } +#[cfg(all(feature = "alloc"))] +#[test] +fn decode_custom_algorithm_openssh() { + let key = PublicKey::from_openssh(OPENSSH_OPAQUE_EXAMPLE).unwrap(); + assert!( + matches!(key.algorithm(), Algorithm::Other(name) if name.as_str() == "name@example.com") + ); + + let opaque_key = key.key_data().other().unwrap(); + assert_eq!( + &hex!("888f24ee17adfed0091e67e485fb9844cfed6072cac1d06390e4005f5015b44f"), + opaque_key.as_ref(), + ); + + assert_eq!("comment@example.com", key.comment()); + + assert_eq!( + "SHA256:8GV7v5qOHG9invseKCx0NVwFocNL0MwdyRC9bfjTFGs", + &key.fingerprint(Default::default()).to_string(), + ); +} + #[cfg(feature = "alloc")] #[test] fn encode_dsa_openssh() { From f676aa51f944c7ef4ff2d79f7503f57608f6b00b Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Tue, 11 Jul 2023 12:05:49 +0100 Subject: [PATCH 04/10] ssh-key: move `AlgorithmName` to a separate module. --- ssh-key/src/algorithm.rs | 122 ++-------------------------------- ssh-key/src/algorithm/name.rs | 112 +++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 116 deletions(-) create mode 100644 ssh-key/src/algorithm/name.rs diff --git a/ssh-key/src/algorithm.rs b/ssh-key/src/algorithm.rs index 356064b6..4e9bfdbd 100644 --- a/ssh-key/src/algorithm.rs +++ b/ssh-key/src/algorithm.rs @@ -1,5 +1,8 @@ //! Algorithm support. +#[cfg(feature = "alloc")] +mod name; + use crate::{Error, Result}; use core::{fmt, str}; use encoding::{Label, LabelError}; @@ -8,9 +11,11 @@ use encoding::{Label, LabelError}; use { alloc::vec::Vec, sha2::{Digest, Sha256, Sha512}, - tinystr::TinyAsciiStr, }; +#[cfg(feature = "alloc")] +pub use name::AlgorithmName; + /// bcrypt-pbkdf const BCRYPT: &str = "bcrypt"; @@ -77,121 +82,6 @@ const SK_ECDSA_SHA2_P256: &str = "sk-ecdsa-sha2-nistp256@openssh.com"; /// U2F/FIDO security key with Ed25519 const SK_SSH_ED25519: &str = "sk-ssh-ed25519@openssh.com"; -/// The suffix added to the `name` in a `name@domainname` algorithm string identifier. -#[cfg(feature = "alloc")] -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 -#[cfg(feature = "alloc")] -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). -#[cfg(feature = "alloc")] -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 -// -// NOTE: We use TinyAsciiStr instead of String to allow Algorithm to implement Copy. -#[cfg(feature = "alloc")] -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub struct AlgorithmName { - /// The string identifier which corresponds to this algorithm. - id: TinyAsciiStr, - /// 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: TinyAsciiStr, -} - -#[cfg(feature = "alloc")] -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) -> core::result::Result { - if id.len() > MAX_CERT_STR_LEN { - return Err(LabelError::new(id)); - } - - let certificate_str = TinyAsciiStr::from_str(id).map_err(|_| LabelError::new(id))?; - - // Derive the algorithm name from the certificate format string identifier: - 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)); - } - - let name = name - .strip_suffix(CERT_STR_SUFFIX) - .ok_or_else(|| LabelError::new(id))?; - - let algorithm_name = - TinyAsciiStr::from_str(&format!("{name}@{domain}")).map_err(|_| LabelError::new(id))?; - - Ok(Self { - id: algorithm_name, - certificate_str, - }) - } -} - -#[cfg(feature = "alloc")] -impl str::FromStr for AlgorithmName { - type Err = LabelError; - - fn from_str(id: &str) -> core::result::Result { - if id.len() > MAX_ALGORITHM_NAME_LEN { - return Err(LabelError::new(id)); - } - - let algorithm_name = TinyAsciiStr::from_str(id).map_err(|_| LabelError::new(id))?; - - // Derive the certificate format string identifier from the algorithm name: - 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)); - } - - let certificate_str = TinyAsciiStr::from_str(&format!("{name}{CERT_STR_SUFFIX}@{domain}")) - .map_err(|_| LabelError::new(id))?; - - Ok(Self { - id: algorithm_name, - certificate_str, - }) - } -} - /// SSH key algorithms. /// /// This type provides a registry of supported digital signature algorithms diff --git a/ssh-key/src/algorithm/name.rs b/ssh-key/src/algorithm/name.rs new file mode 100644 index 00000000..0b74ecf2 --- /dev/null +++ b/ssh-key/src/algorithm/name.rs @@ -0,0 +1,112 @@ +use core::str; +use encoding::LabelError; +use tinystr::TinyAsciiStr; + +/// 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 +// +// NOTE: We use TinyAsciiStr instead of String to allow Algorithm to implement Copy. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct AlgorithmName { + /// The string identifier which corresponds to this algorithm. + id: TinyAsciiStr, + /// 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: TinyAsciiStr, +} + +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 { + if id.len() > MAX_CERT_STR_LEN { + return Err(LabelError::new(id)); + } + + let certificate_str = TinyAsciiStr::from_str(id).map_err(|_| LabelError::new(id))?; + + // Derive the algorithm name from the certificate format string identifier: + 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)); + } + + let name = name + .strip_suffix(CERT_STR_SUFFIX) + .ok_or_else(|| LabelError::new(id))?; + + let algorithm_name = + TinyAsciiStr::from_str(&format!("{name}@{domain}")).map_err(|_| LabelError::new(id))?; + + Ok(Self { + id: algorithm_name, + certificate_str, + }) + } +} + +impl str::FromStr for AlgorithmName { + type Err = LabelError; + + fn from_str(id: &str) -> Result { + if id.len() > MAX_ALGORITHM_NAME_LEN { + return Err(LabelError::new(id)); + } + + let algorithm_name = TinyAsciiStr::from_str(id).map_err(|_| LabelError::new(id))?; + + // Derive the certificate format string identifier from the algorithm name: + 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)); + } + + let certificate_str = TinyAsciiStr::from_str(&format!("{name}{CERT_STR_SUFFIX}@{domain}")) + .map_err(|_| LabelError::new(id))?; + + Ok(Self { + id: algorithm_name, + certificate_str, + }) + } +} From 8d739884ca6f2efa91dc821780aaaefbd461ea7e Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Tue, 11 Jul 2023 12:02:04 +0100 Subject: [PATCH 05/10] ssh-key: remove the `tinystr` dependency. This replaces the `tinystr` dependency with a hand-rolled `AsciiStr` implementation. --- Cargo.lock | 21 ------------ ssh-key/Cargo.toml | 2 -- ssh-key/src/algorithm/name.rs | 63 ++++++++++++++++++++++++----------- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1179718a..ebdbcdf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,17 +240,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "dsa" version = "0.6.1" @@ -803,7 +792,6 @@ dependencies = [ "ssh-cipher", "ssh-encoding", "subtle", - "tinystr", "zeroize", ] @@ -824,15 +812,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tinystr" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ac3f5b6856e931e15e07b478e98c8045239829a65f9156d4fa7e7788197a5ef" -dependencies = [ - "displaydoc", -] - [[package]] name = "typenum" version = "1.16.0" diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index 2e35341f..097993d4 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -36,7 +36,6 @@ rsa = { version = "0.9", optional = true, default-features = false, features = [ sec1 = { version = "0.7", optional = true, default-features = false, features = ["point"] } serde = { version = "1", optional = true } sha1 = { version = "0.10", optional = true, default-features = false } -tinystr = { version = "0.7.1", optional = true, default-features = false } [dev-dependencies] hex-literal = "0.4.1" @@ -48,7 +47,6 @@ alloc = [ "encoding/alloc", "signature/alloc", "zeroize/alloc", - "dep:tinystr" ] std = [ "alloc", diff --git a/ssh-key/src/algorithm/name.rs b/ssh-key/src/algorithm/name.rs index 0b74ecf2..736aebcf 100644 --- a/ssh-key/src/algorithm/name.rs +++ b/ssh-key/src/algorithm/name.rs @@ -1,6 +1,6 @@ -use core::str; +use core::ops::Deref; +use core::str::{self, FromStr}; use encoding::LabelError; -use tinystr::TinyAsciiStr; /// The suffix added to the `name` in a `name@domainname` algorithm string identifier. const CERT_STR_SUFFIX: &str = "-cert-v01"; @@ -16,6 +16,39 @@ const MAX_ALGORITHM_NAME_LEN: usize = 64; /// algorithm name). const MAX_CERT_STR_LEN: usize = MAX_ALGORITHM_NAME_LEN + CERT_STR_SUFFIX.len(); +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +struct AsciiStr { + inner: [u8; N], + len: usize, +} + +impl FromStr for AsciiStr { + type Err = LabelError; + + fn from_str(id: &str) -> Result { + if id.len() > N || !id.is_ascii() { + return Err(LabelError::new(id)); + } + + let len = id.len(); + let mut inner = [0u8; N]; + inner[..len].copy_from_slice(id.as_bytes()); + + Ok(Self { inner, len }) + } +} + +impl Deref for AsciiStr { + type Target = str; + #[inline] + fn deref(&self) -> &str { + // This conversion should **not** fail, as `self.inner` can only ever be constructed from + // valid `&str`s. + str::from_utf8(&self.inner[..self.len]) + .expect("AsciiStr can only be built from valid strings") + } +} + /// A string representing an additional algorithm name in the `name@domainname` format (see /// [RFC4251 § 6]). /// @@ -30,16 +63,16 @@ const MAX_CERT_STR_LEN: usize = MAX_ALGORITHM_NAME_LEN + CERT_STR_SUFFIX.len(); /// /// [RFC4251 § 6]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6 // -// NOTE: We use TinyAsciiStr instead of String to allow Algorithm to implement Copy. +// NOTE: We use AsciiStr instead of String to allow Algorithm to implement Copy. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct AlgorithmName { /// The string identifier which corresponds to this algorithm. - id: TinyAsciiStr, + id: AsciiStr, /// 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: TinyAsciiStr, + certificate_str: AsciiStr, } impl AlgorithmName { @@ -55,11 +88,7 @@ impl AlgorithmName { /// Create a new [`AlgorithmName`] from an OpenSSH certificate format string identifier. pub fn from_certificate_str(id: &str) -> Result { - if id.len() > MAX_CERT_STR_LEN { - return Err(LabelError::new(id)); - } - - let certificate_str = TinyAsciiStr::from_str(id).map_err(|_| LabelError::new(id))?; + let certificate_str = AsciiStr::from_str(id)?; // Derive the algorithm name from the certificate format string identifier: let (name, domain) = id.split_once('@').ok_or_else(|| LabelError::new(id))?; @@ -73,8 +102,7 @@ impl AlgorithmName { .strip_suffix(CERT_STR_SUFFIX) .ok_or_else(|| LabelError::new(id))?; - let algorithm_name = - TinyAsciiStr::from_str(&format!("{name}@{domain}")).map_err(|_| LabelError::new(id))?; + let algorithm_name = AsciiStr::from_str(&format!("{name}@{domain}"))?; Ok(Self { id: algorithm_name, @@ -83,15 +111,11 @@ impl AlgorithmName { } } -impl str::FromStr for AlgorithmName { +impl FromStr for AlgorithmName { type Err = LabelError; fn from_str(id: &str) -> Result { - if id.len() > MAX_ALGORITHM_NAME_LEN { - return Err(LabelError::new(id)); - } - - let algorithm_name = TinyAsciiStr::from_str(id).map_err(|_| LabelError::new(id))?; + let algorithm_name = AsciiStr::from_str(id)?; // Derive the certificate format string identifier from the algorithm name: let (name, domain) = id.split_once('@').ok_or_else(|| LabelError::new(id))?; @@ -101,8 +125,7 @@ impl str::FromStr for AlgorithmName { return Err(LabelError::new(id)); } - let certificate_str = TinyAsciiStr::from_str(&format!("{name}{CERT_STR_SUFFIX}@{domain}")) - .map_err(|_| LabelError::new(id))?; + let certificate_str = AsciiStr::from_str(&format!("{name}{CERT_STR_SUFFIX}@{domain}"))?; Ok(Self { id: algorithm_name, From 98de06009fd94123c3980fff5e33ff6ed895dfa6 Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Tue, 11 Jul 2023 12:33:53 +0100 Subject: [PATCH 06/10] ssh-key: Use `Box` in `Error::UnsupportedAlgorithm`. This ensures the size of `Error::UnsupportedAlgorithm` doesn't exceed the maximum allowed by the `result_large_err` clippy lint. --- ssh-key/src/algorithm.rs | 5 ++++- ssh-key/src/error.rs | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ssh-key/src/algorithm.rs b/ssh-key/src/algorithm.rs index 4e9bfdbd..3f7d4031 100644 --- a/ssh-key/src/algorithm.rs +++ b/ssh-key/src/algorithm.rs @@ -9,6 +9,7 @@ use encoding::{Label, LabelError}; #[cfg(feature = "alloc")] use { + alloc::boxed::Box, alloc::vec::Vec, sha2::{Digest, Sha256, Sha512}, }; @@ -254,7 +255,9 @@ impl Algorithm { /// Return an error indicating this algorithm is unsupported. #[allow(dead_code)] pub(crate) fn unsupported_error(self) -> Error { - Error::AlgorithmUnsupported { algorithm: self } + Error::AlgorithmUnsupported { + algorithm: Box::new(self), + } } } diff --git a/ssh-key/src/error.rs b/ssh-key/src/error.rs index 1428db43..9093090f 100644 --- a/ssh-key/src/error.rs +++ b/ssh-key/src/error.rs @@ -4,7 +4,7 @@ use crate::Algorithm; use core::fmt; #[cfg(feature = "alloc")] -use crate::certificate; +use {crate::certificate, alloc::boxed::Box}; /// Result type with `ssh-key`'s [`Error`] as the error type. pub type Result = core::result::Result; @@ -27,6 +27,14 @@ pub enum Error { /// a given usage pattern or context. AlgorithmUnsupported { /// Algorithm identifier. + // + // NOTE: with the `alloc` feature is enabled, `Algorithm` has a large `Algorithm::Other` + // variant (168 bytes), so we box it to keep the size of `Error` small. + #[cfg(feature = "alloc")] + algorithm: Box, + + /// Algorithm identifier. + #[cfg(not(feature = "alloc"))] algorithm: Algorithm, }, From df169ebeccb5a599f41fdeebc1fb1ce0be016095 Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Mon, 17 Jul 2023 17:04:01 +0100 Subject: [PATCH 07/10] ssh-key: do not derive `Copy` for `Algorithm`. Update `Algorithm` in preparation for the change that will make `AlgorithmName` (and thus `Algorithm`) non-`Copy`. --- ssh-key/src/algorithm.rs | 2 +- ssh-key/src/public/opaque.rs | 2 +- ssh-key/src/signature.rs | 6 +++--- ssh-key/tests/certificate_builder.rs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ssh-key/src/algorithm.rs b/ssh-key/src/algorithm.rs index 3f7d4031..3a1b2292 100644 --- a/ssh-key/src/algorithm.rs +++ b/ssh-key/src/algorithm.rs @@ -87,7 +87,7 @@ const SK_SSH_ED25519: &str = "sk-ssh-ed25519@openssh.com"; /// /// 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 diff --git a/ssh-key/src/public/opaque.rs b/ssh-key/src/public/opaque.rs index 2526646e..b0c64115 100644 --- a/ssh-key/src/public/opaque.rs +++ b/ssh-key/src/public/opaque.rs @@ -42,7 +42,7 @@ impl OpaquePublicKey { /// Get the [`Algorithm`] for this public key type. pub fn algorithm(&self) -> Algorithm { - self.algorithm + self.algorithm.clone() } /// Decode [`OpaquePublicKey`] for the specified algorithm. diff --git a/ssh-key/src/signature.rs b/ssh-key/src/signature.rs index a86206e6..47232422 100644 --- a/ssh-key/src/signature.rs +++ b/ssh-key/src/signature.rs @@ -131,7 +131,7 @@ impl Signature { /// Get the [`Algorithm`] associated with this signature. pub fn algorithm(&self) -> Algorithm { - self.algorithm + self.algorithm.clone() } /// Get the raw signature as bytes. @@ -531,7 +531,7 @@ impl TryFrom<&Signature> for p256::ecdsa::Signature { _ => Err(Error::Crypto), } } - _ => Err(signature.algorithm.unsupported_error()), + _ => Err(signature.algorithm.clone().unsupported_error()), } } } @@ -561,7 +561,7 @@ impl TryFrom<&Signature> for p384::ecdsa::Signature { _ => Err(Error::Crypto), } } - _ => Err(signature.algorithm.unsupported_error()), + _ => Err(signature.algorithm.clone().unsupported_error()), } } } diff --git a/ssh-key/tests/certificate_builder.rs b/ssh-key/tests/certificate_builder.rs index af726fb5..7527c8ed 100644 --- a/ssh-key/tests/certificate_builder.rs +++ b/ssh-key/tests/certificate_builder.rs @@ -105,8 +105,8 @@ fn ecdsa_nistp256_sign_and_verify() { let algorithm = Algorithm::Ecdsa { curve: EcdsaCurve::NistP256, }; - let ca_key = PrivateKey::random(&mut rng, algorithm).unwrap(); - let subject_key = PrivateKey::random(&mut rng, algorithm).unwrap(); + let ca_key = PrivateKey::random(&mut rng, algorithm.clone()).unwrap(); + let subject_key = PrivateKey::random(&mut rng, algorithm.clone()).unwrap(); let mut cert_builder = certificate::Builder::new_with_random_nonce( &mut rng, subject_key.public_key(), From 38b818993c8c62cdc68bcd5eea7dbf134af4d05f Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Mon, 17 Jul 2023 17:08:33 +0100 Subject: [PATCH 08/10] ssh-key: remove `AsciiStr`, make `AlgorithmName` non-`Copy`. --- ssh-key/src/algorithm/name.rs | 59 +++++++++-------------------------- 1 file changed, 14 insertions(+), 45 deletions(-) diff --git a/ssh-key/src/algorithm/name.rs b/ssh-key/src/algorithm/name.rs index 736aebcf..074ddc05 100644 --- a/ssh-key/src/algorithm/name.rs +++ b/ssh-key/src/algorithm/name.rs @@ -1,4 +1,4 @@ -use core::ops::Deref; +use alloc::string::String; use core::str::{self, FromStr}; use encoding::LabelError; @@ -16,39 +16,6 @@ const MAX_ALGORITHM_NAME_LEN: usize = 64; /// algorithm name). const MAX_CERT_STR_LEN: usize = MAX_ALGORITHM_NAME_LEN + CERT_STR_SUFFIX.len(); -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -struct AsciiStr { - inner: [u8; N], - len: usize, -} - -impl FromStr for AsciiStr { - type Err = LabelError; - - fn from_str(id: &str) -> Result { - if id.len() > N || !id.is_ascii() { - return Err(LabelError::new(id)); - } - - let len = id.len(); - let mut inner = [0u8; N]; - inner[..len].copy_from_slice(id.as_bytes()); - - Ok(Self { inner, len }) - } -} - -impl Deref for AsciiStr { - type Target = str; - #[inline] - fn deref(&self) -> &str { - // This conversion should **not** fail, as `self.inner` can only ever be constructed from - // valid `&str`s. - str::from_utf8(&self.inner[..self.len]) - .expect("AsciiStr can only be built from valid strings") - } -} - /// A string representing an additional algorithm name in the `name@domainname` format (see /// [RFC4251 § 6]). /// @@ -62,17 +29,15 @@ impl Deref for AsciiStr { /// implement all of them here. /// /// [RFC4251 § 6]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6 -// -// NOTE: We use AsciiStr instead of String to allow Algorithm to implement Copy. -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct AlgorithmName { /// The string identifier which corresponds to this algorithm. - id: AsciiStr, + 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: AsciiStr, + certificate_str: String, } impl AlgorithmName { @@ -88,7 +53,9 @@ impl AlgorithmName { /// Create a new [`AlgorithmName`] from an OpenSSH certificate format string identifier. pub fn from_certificate_str(id: &str) -> Result { - let certificate_str = AsciiStr::from_str(id)?; + if id.len() > MAX_CERT_STR_LEN || !id.is_ascii() { + return Err(LabelError::new(id)); + } // Derive the algorithm name from the certificate format string identifier: let (name, domain) = id.split_once('@').ok_or_else(|| LabelError::new(id))?; @@ -102,11 +69,11 @@ impl AlgorithmName { .strip_suffix(CERT_STR_SUFFIX) .ok_or_else(|| LabelError::new(id))?; - let algorithm_name = AsciiStr::from_str(&format!("{name}@{domain}"))?; + let algorithm_name = format!("{name}@{domain}"); Ok(Self { id: algorithm_name, - certificate_str, + certificate_str: id.into(), }) } } @@ -115,7 +82,9 @@ impl FromStr for AlgorithmName { type Err = LabelError; fn from_str(id: &str) -> Result { - let algorithm_name = AsciiStr::from_str(id)?; + if id.len() > MAX_ALGORITHM_NAME_LEN || !id.is_ascii() { + return Err(LabelError::new(id)); + } // Derive the certificate format string identifier from the algorithm name: let (name, domain) = id.split_once('@').ok_or_else(|| LabelError::new(id))?; @@ -125,10 +94,10 @@ impl FromStr for AlgorithmName { return Err(LabelError::new(id)); } - let certificate_str = AsciiStr::from_str(&format!("{name}{CERT_STR_SUFFIX}@{domain}"))?; + let certificate_str = format!("{name}{CERT_STR_SUFFIX}@{domain}"); Ok(Self { - id: algorithm_name, + id: id.into(), certificate_str, }) } From 46c9802714042f2e887d6f06d30bee9faf394a93 Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Mon, 17 Jul 2023 17:27:51 +0100 Subject: [PATCH 09/10] ssh-key: Reduce code duplication in `AlgorithmName` impl. --- ssh-key/src/algorithm/name.rs | 45 +++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/ssh-key/src/algorithm/name.rs b/ssh-key/src/algorithm/name.rs index 074ddc05..d85bd3b5 100644 --- a/ssh-key/src/algorithm/name.rs +++ b/ssh-key/src/algorithm/name.rs @@ -53,18 +53,10 @@ impl AlgorithmName { /// Create a new [`AlgorithmName`] from an OpenSSH certificate format string identifier. pub fn from_certificate_str(id: &str) -> Result { - if id.len() > MAX_CERT_STR_LEN || !id.is_ascii() { - return Err(LabelError::new(id)); - } + validate_algorithm_id(id, MAX_CERT_STR_LEN)?; // Derive the algorithm name from the certificate format string identifier: - 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)); - } - + let (name, domain) = split_algorithm_id(id)?; let name = name .strip_suffix(CERT_STR_SUFFIX) .ok_or_else(|| LabelError::new(id))?; @@ -82,18 +74,10 @@ impl FromStr for AlgorithmName { type Err = LabelError; fn from_str(id: &str) -> Result { - if id.len() > MAX_ALGORITHM_NAME_LEN || !id.is_ascii() { - return Err(LabelError::new(id)); - } + validate_algorithm_id(id, MAX_ALGORITHM_NAME_LEN)?; // Derive the certificate format string identifier from the algorithm name: - 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)); - } - + let (name, domain) = split_algorithm_id(id)?; let certificate_str = format!("{name}{CERT_STR_SUFFIX}@{domain}"); Ok(Self { @@ -102,3 +86,24 @@ impl FromStr for AlgorithmName { }) } } + +/// 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)) +} From 2bd496bb03ba8ac93fc63de50bd535765b0e520e Mon Sep 17 00:00:00 2001 From: Gabriela Moldovan Date: Mon, 17 Jul 2023 17:09:14 +0100 Subject: [PATCH 10/10] Revert "ssh-key: Use `Box` in `Error::UnsupportedAlgorithm`." This reverts commit 98de06009fd94123c3980fff5e33ff6ed895dfa6. --- ssh-key/src/algorithm.rs | 5 +---- ssh-key/src/error.rs | 10 +--------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/ssh-key/src/algorithm.rs b/ssh-key/src/algorithm.rs index 3a1b2292..100434ba 100644 --- a/ssh-key/src/algorithm.rs +++ b/ssh-key/src/algorithm.rs @@ -9,7 +9,6 @@ use encoding::{Label, LabelError}; #[cfg(feature = "alloc")] use { - alloc::boxed::Box, alloc::vec::Vec, sha2::{Digest, Sha256, Sha512}, }; @@ -255,9 +254,7 @@ impl Algorithm { /// Return an error indicating this algorithm is unsupported. #[allow(dead_code)] pub(crate) fn unsupported_error(self) -> Error { - Error::AlgorithmUnsupported { - algorithm: Box::new(self), - } + Error::AlgorithmUnsupported { algorithm: self } } } diff --git a/ssh-key/src/error.rs b/ssh-key/src/error.rs index 9093090f..1428db43 100644 --- a/ssh-key/src/error.rs +++ b/ssh-key/src/error.rs @@ -4,7 +4,7 @@ use crate::Algorithm; use core::fmt; #[cfg(feature = "alloc")] -use {crate::certificate, alloc::boxed::Box}; +use crate::certificate; /// Result type with `ssh-key`'s [`Error`] as the error type. pub type Result = core::result::Result; @@ -27,14 +27,6 @@ pub enum Error { /// a given usage pattern or context. AlgorithmUnsupported { /// Algorithm identifier. - // - // NOTE: with the `alloc` feature is enabled, `Algorithm` has a large `Algorithm::Other` - // variant (168 bytes), so we box it to keep the size of `Error` small. - #[cfg(feature = "alloc")] - algorithm: Box, - - /// Algorithm identifier. - #[cfg(not(feature = "alloc"))] algorithm: Algorithm, },