Skip to content

Commit

Permalink
ssh-key: create Keypair and KeyData variants for custom algorithms.
Browse files Browse the repository at this point in the history
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 committed Jul 9, 2023
1 parent 5896d12 commit ee4e023
Show file tree
Hide file tree
Showing 10 changed files with 398 additions and 4 deletions.
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
155 changes: 155 additions & 0 deletions ssh-key/src/private/opaque.rs
Original file line number Diff line number Diff line change
@@ -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<u8>);

/// 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<u8>, 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<Self> {
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<Self> {
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<Self> {
let public = OpaquePublicKeyBytes::decode(reader)?;
let private = OpaquePrivateKeyBytes::decode(reader)?;

Ok(Self { public, private })
}
}

impl Encode for OpaqueKeypair {
fn encoded_len(&self) -> encoding::Result<usize> {
[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<usize> {
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
}
}
8 changes: 7 additions & 1 deletion ssh-key/src/public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ mod ecdsa;
mod ed25519;
mod key_data;
#[cfg(feature = "alloc")]
mod opaque;
#[cfg(feature = "alloc")]
mod rsa;
mod sk;
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};
Expand Down
29 changes: 28 additions & 1 deletion ssh-key/src/public/key_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand All @@ -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(),
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Self> {
match algorithm {
Expand All @@ -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),
}
Expand All @@ -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(),
}
}

Expand All @@ -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),
}
}
}
Expand Down
Loading

0 comments on commit ee4e023

Please sign in to comment.