diff --git a/Cargo.toml b/Cargo.toml index d9d8206a9b995fb09018e150c54bb4ba8d04b9e2..a918fc6b1594f0aa95d5db22774184d6a6cd44cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ no-panic = "0.1" ascii = { version = "1.0", default-features = false } getrandom = "0.2" +rand_core = { version = "0.6", default-features = false, features = ["getrandom"]} ctr = { version = "0.9", features = ["zeroize"] } aes = { version = "0.8", features = ["zeroize"] } @@ -52,7 +53,9 @@ subtle = { version = "2.4", default-features = false } # ed25519/x25519 # fork allows hashing by parts (sign/verify from sshwire), and zeroize salty = { version = "0.2", git = "https://github.com/mkj/salty", branch = "sunset" } -ssh-key = { version = "0.5", default-features = false, optional = true } +rsa = { version = "0.8", default-features = false, optional = true, features = ["sha2"] } +# TODO: getrandom feature is a workaround for missing ssh-key dependency with rsa. fixed in pending 0.6 +ssh-key = { version = "0.5", default-features = false, optional = true, features = ["getrandom"] } embedded-io = { version = "0.4", optional = true } @@ -60,9 +63,10 @@ embedded-io = { version = "0.4", optional = true } pretty-hex = { version = "0.3", default-features = false } [features] -std = ["snafu/std", "snafu/backtraces" ] +std = ["snafu/std", "snafu/backtraces", "rsa"] +rsa = ["dep:rsa", "ssh-key/rsa"] # allows conversion to/from OpenSSH key formats -openssh-key = ["dep:ssh-key"] +openssh-key = ["ssh-key"] # implements embedded_io::Error for sunset::Error embedded-io = ["dep:embedded-io"] diff --git a/async/Cargo.toml b/async/Cargo.toml index 424553642f19f3bf411af3900ae76f169fabfec8..2eaa8c28bd3e3a066bd825bd74256ca991bfdd7d 100644 --- a/async/Cargo.toml +++ b/async/Cargo.toml @@ -36,6 +36,10 @@ heapless = "0.7.10" pretty-hex = "0.3" # snafu = { version = "0.7", default-features = true } +[features] +# rsa is implied by sunset/std +# rsa = ["sunset/rsa"] + [dev-dependencies] anyhow = { version = "1.0" } pretty-hex = "0.3" diff --git a/async/src/agent.rs b/async/src/agent.rs index 9fc0868520e371ee755b9ed950563658565e199f..d022a7c4a0fd1c73dc5fc88b005c4beaf08f3821 100644 --- a/async/src/agent.rs +++ b/async/src/agent.rs @@ -165,8 +165,11 @@ impl AgentClient { } pub async fn sign_auth(&mut self, key: &SignKey, msg: &AuthSigMsg<'_>) -> Result<OwnedSig> { - // TODO: rsa needs SSH_AGENT_FLAG_RSA_SHA2_256 - let flags = 0u32; + let flags = match key { + SignKey::AgentRSA(_) => SSH_AGENT_FLAG_RSA_SHA2_256, + _ => 0, + }; + trace!("flags {key:?}"); let r = AgentRequest::SignRequest(AgentSignRequest { key_blob: Blob(key.pubkey()), msg: Blob(msg), diff --git a/src/cliauth.rs b/src/cliauth.rs index 61327c48ad35ebdd85c15067fce872c7827199d8..18b44402b5353b6543fe1d0b54c5adc8ef5fb2c1 100644 --- a/src/cliauth.rs +++ b/src/cliauth.rs @@ -144,19 +144,32 @@ impl CliAuth { &mut self, b: &mut impl CliBehaviour, ) -> Option<Req> { - let k = b.next_authkey().unwrap_or_else(|e| { - warn!("Error getting pubkey for auth"); - None - }); - - match k { - Some(key) => { - Some(Req::PubKey { key }) - } - None => { - trace!("stop iterating pubkeys"); - self.try_pubkey = false; + loop { + let k = b.next_authkey().unwrap_or_else(|e| { + warn!("Error getting pubkey for auth"); None + }); + + match k { + Some(key) => { + #[cfg(feature = "rsa")] + match key { + SignKey::RSA(_) | SignKey::AgentRSA(_) => { + if !self.allow_rsa_sha2 { + trace!("Skipping rsa key, no ext-info"); + continue + } + } + _ => (), + } + + break Some(Req::PubKey { key }) + } + None => { + trace!("stop iterating pubkeys"); + self.try_pubkey = false; + break None + } } } } @@ -297,6 +310,8 @@ impl CliAuth { pub fn handle_ext_info(&mut self, p: &packets::ExtInfo<'_>) { if let Some(ref algs) = p.server_sig_algs { + // we only worry about rsa-sha256, assuming other older key types are fine + // OK unwrap: is a remote namelist self.allow_rsa_sha2 = algs.has_algo(SSH_NAME_RSA_SHA256).unwrap(); trace!("setting allow_rsa_sha2 = {}", self.allow_rsa_sha2); diff --git a/src/config.rs b/src/config.rs index 659702ff290235cedb866079e1839ee23c3a3500..389dbc0e784b5727e99572fc6476fcd5a5760ae2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,3 +18,5 @@ pub const MAX_TERM: usize = 32; pub const DEFAULT_TERM: &str = "xterm"; +pub const RSA_DEFAULT_KEYSIZE: usize = 2048; +pub const RSA_MIN_KEYSIZE: usize = 1024; diff --git a/src/error.rs b/src/error.rs index 897b774090fa7bb518e9b074b2704f69b77bec28..c0417b09894fa3f7c6828aefd7eebf3edda340fb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -47,6 +47,9 @@ pub enum Error { /// Error in received SSH protocol. Will disconnect. SSHProtoError, + /// Received a key with invalid structure, or too large. + BadKeyFormat, + /// Remote peer isn't SSH NotSSH, diff --git a/src/kex.rs b/src/kex.rs index 540a8b7b2ee1baccf9b26e3f34ecea66f2dc69ab..4f1fd5f66e364397bf367190d02229375fffd029 100644 --- a/src/kex.rs +++ b/src/kex.rs @@ -40,7 +40,7 @@ const marker_only_kexs: &[&'static str] = const fixed_options_hostsig: &[&'static str] = &[ SSH_NAME_ED25519, - #[cfg(rsa)] + #[cfg(feature = "rsa")] SSH_NAME_RSA_SHA256, ]; @@ -65,7 +65,7 @@ impl AlgoConfig { let mut kexs: LocalNames = fixed_options_kex.try_into().unwrap(); // Only clients are interested in ext-info - // TODO perhaps it could go behind cfg(rsa)? + // TODO perhaps it could go behind cfg rsa? if is_client { // OK unwrap: static arrays are < MAX_LOCAL_NAMES+slack kexs.0.push(SSH_NAME_EXT_INFO_C).unwrap(); @@ -410,6 +410,7 @@ impl Kex { false => p.kex.has_algo(SSH_NAME_EXT_INFO_C).unwrap(), }; + debug!("hostsig {:?} vs {:?}", p.hostsig, conf.hostsig); let hostsig_method = p .hostsig .first_match(is_client, &conf.hostsig)? diff --git a/src/packets.rs b/src/packets.rs index 087abb27f9ce7ceef1ae78ef8108bd51a441bae7..d24c615fe7703b4af449bea43532f154e64122fd 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -11,6 +11,7 @@ use { }; use core::fmt; +use core::fmt::{Debug, Display}; use heapless::String; use pretty_hex::PrettyHex; @@ -25,6 +26,9 @@ use sign::{SigType, OwnedSig}; use sshwire::{SSHEncode, SSHDecode, SSHSource, SSHSink, WireResult, WireError}; use sshwire::{SSHEncodeEnum, SSHDecodeEnum}; +#[cfg(feature = "rsa")] +use rsa::PublicKeyParts; + // Any `enum` needs to have special handling to select a variant when deserializing. // This is mostly done with `#[sshwire(...)]` attributes. @@ -208,7 +212,7 @@ pub struct MethodPassword<'a> { } // Don't print password -impl fmt::Debug for MethodPassword<'_>{ +impl Debug for MethodPassword<'_>{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("MethodPassword") .field("change", &self.change) @@ -295,8 +299,11 @@ pub struct UserauthBanner<'a> { pub enum PubKey<'a> { #[sshwire(variant = SSH_NAME_ED25519)] Ed25519(Ed25519PubKey<'a>), + + #[cfg(feature = "rsa")] #[sshwire(variant = SSH_NAME_RSA)] - RSA(RSAPubKey<'a>), + RSA(RSAPubKey), + #[sshwire(unknown)] Unknown(Unknown<'a>), } @@ -306,6 +313,7 @@ impl PubKey<'_> { pub fn algorithm_name(&self) -> Result<&str, &Unknown<'_>> { match self { PubKey::Ed25519(_) => Ok(SSH_NAME_ED25519), + #[cfg(feature = "rsa")] PubKey::RSA(_) => Ok(SSH_NAME_RSA), PubKey::Unknown(u) => Err(u), } @@ -329,16 +337,27 @@ impl PubKey<'_> { } } +// ssh_key::PublicKey is used for known_hosts comparisons #[cfg(feature = "openssh-key")] impl TryFrom<&PubKey<'_>> for ssh_key::PublicKey { type Error = Error; - fn try_from(k: &PubKey<'_>) -> Result<Self> { + fn try_from(k: &PubKey) -> Result<Self> { match k { PubKey::Ed25519(e) => { let eb: &[u8; 32] = e.key.0.try_into().map_err(|_| Error::BadKey)?; Ok(ssh_key::public::Ed25519PublicKey(*eb).into()) } - _ => Err(Error::msg("Unsupported OpenSSH key")) + + #[cfg(feature = "rsa")] + PubKey::RSA(r) => { + let k = ssh_key::public::RsaPublicKey { + n: r.key.n().try_into().map_err(|_| Error::BadKey)?, + e: r.key.e().try_into().map_err(|_| Error::BadKey)?, + }; + Ok(k.into()) + } + + u => Err(Error::msg("Unsupported {u} OpenSSH key")) } } } @@ -350,41 +369,64 @@ pub struct Ed25519PubKey<'a> { impl TryFrom<&Ed25519PubKey<'_>> for salty::PublicKey { type Error = Error; - fn try_from(k: &Ed25519PubKey<'_>) -> Result<Self> { + fn try_from(k: &Ed25519PubKey) -> Result<Self> { let b: [u8; 32] = k.key.0.try_into().map_err(|_| Error::BadKey)?; (&b).try_into().map_err(|_| Error::BadKey) } } +#[cfg(feature = "rsa")] +#[derive(Clone, PartialEq)] +pub struct RSAPubKey { + // mpint e + // mpint n + pub key: rsa::RsaPublicKey, +} -#[derive(Debug, Clone, PartialEq, SSHEncode, SSHDecode)] -pub struct RSAPubKey<'a> { - pub e: BinString<'a>, - pub n: BinString<'a>, -} - -// #[cfg(feature = "rsa")] -// impl TryFrom<RsaPubKey<'_> for rsa::RsaPublicKey { -// fn try_from(value: RsaPubKey<'_>) -> Result<Self, Self::Error> { -// use rsa::BigUint; -// rsa::RsaPublickey::new( -// BigUint::from_bytes_be(n.0), -// BigUint::from_bytes_be(e.0), -// ) -// .map_err(|e| { -// debug!("Bad RSA key: {e}"); -// Error::BadKey -// }) -// } -// } +#[cfg(feature = "rsa")] +impl SSHEncode for RSAPubKey { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + use rsa::PublicKeyParts; + self.key.e().enc(s)?; + self.key.n().enc(s)?; + Ok(()) + } +} + +#[cfg(feature = "rsa")] +impl<'de> SSHDecode<'de> for RSAPubKey { + fn dec<S>(s: &mut S) -> WireResult<Self> where S: SSHSource<'de> { + use rsa::PublicKeyParts; + let e = SSHDecode::dec(s)?; + let n = SSHDecode::dec(s)?; + let key = rsa::RsaPublicKey::new(n, e) + .map_err(|e| { + debug!("Invalid RSA public key: {e}"); + WireError::BadKeyFormat + })?; + Ok(Self { key }) + } +} + +#[cfg(feature = "rsa")] +impl Debug for RSAPubKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RSAPubKey") + .field(".key bits", &(self.key.n().bits())) + .finish_non_exhaustive() + } +} #[derive(Debug, SSHEncode, SSHDecode)] #[sshwire(variant_prefix)] pub enum Signature<'a> { #[sshwire(variant = SSH_NAME_ED25519)] Ed25519(Ed25519Sig<'a>), + + #[cfg(feature = "rsa")] #[sshwire(variant = SSH_NAME_RSA_SHA256)] - RSA256(RSA256Sig<'a>), + RSA(RSASig<'a>), + #[sshwire(unknown)] Unknown(Unknown<'a>), } @@ -394,7 +436,8 @@ impl<'a> Signature<'a> { pub fn algorithm_name(&self) -> Result<&'a str, &Unknown<'a>> { match self { Signature::Ed25519(_) => Ok(SSH_NAME_ED25519), - Signature::RSA256(_) => Ok(SSH_NAME_RSA_SHA256), + #[cfg(feature = "rsa")] + Signature::RSA(_) => Ok(SSH_NAME_RSA_SHA256), Signature::Unknown(u) => Err(u), } } @@ -408,6 +451,7 @@ impl<'a> Signature<'a> { pub fn sig_name_for_pubkey(pubkey: &PubKey) -> Result<&'static str> { match pubkey { PubKey::Ed25519(_) => Ok(SSH_NAME_ED25519), + #[cfg(feature = "rsa")] PubKey::RSA(_) => Ok(SSH_NAME_RSA_SHA256), PubKey::Unknown(u) => { warn!("Unknown key type \"{}\"", u); @@ -419,7 +463,8 @@ impl<'a> Signature<'a> { pub fn sig_type(&self) -> Result<SigType> { match self { Signature::Ed25519(_) => Ok(SigType::Ed25519), - Signature::RSA256(_) => Ok(SigType::RSA256), + #[cfg(feature = "rsa")] + Signature::RSA(_) => Ok(SigType::RSA), Signature::Unknown(u) => { warn!("Unknown signature type \"{}\"", u); Err(Error::UnknownMethod {kind: "signature" }) @@ -431,8 +476,9 @@ impl<'a> Signature<'a> { impl <'a> From<&'a OwnedSig> for Signature<'a> { fn from(s: &'a OwnedSig) -> Self { match s { - OwnedSig::Ed25519(e) => Signature::Ed25519(Ed25519Sig { sig: BinString(e) }), - OwnedSig::_RSA256 => todo!("sig from rsa"), + OwnedSig::Ed25519(s) => Signature::Ed25519(Ed25519Sig { sig: BinString(s) }), + #[cfg(feature = "rsa")] + OwnedSig::RSA(s) => Signature::RSA(RSASig { sig: BinString(s.as_ref()) }) } } } @@ -443,8 +489,9 @@ pub struct Ed25519Sig<'a> { pub sig: BinString<'a>, } +#[cfg(feature = "rsa")] #[derive(Debug, SSHEncode, SSHDecode)] -pub struct RSA256Sig<'a> { +pub struct RSASig<'a> { pub sig: BinString<'a>, } @@ -714,7 +761,15 @@ pub struct DirectTcpip<'a> { #[derive(Clone, PartialEq)] pub struct Unknown<'a>(pub &'a [u8]); -impl core::fmt::Display for Unknown<'_> { +impl<'a> Unknown<'a> { + fn new(u: &'a [u8]) -> Self { + let u = Unknown(u); + trace!("saw unknown variant \"{u}\""); + u + } +} + +impl Display for Unknown<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Ok(s) = sshwire::try_as_ascii_str(self.0) { f.write_str(s) @@ -724,7 +779,7 @@ impl core::fmt::Display for Unknown<'_> { } } -impl core::fmt::Debug for Unknown<'_> { +impl Debug for Unknown<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self}") } diff --git a/src/sign.rs b/src/sign.rs index c6f9e717ae1138d82059a4fdef78ad5fb54a4e58..0cf37d86c716fa7bdfe4d89674a3bb1e0f03cba0 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -5,7 +5,8 @@ use { log::{debug, error, info, log, trace, warn}, }; -use salty::{SecretKey, PublicKey}; +use core::ops::Deref; + use signature::Verifier; use zeroize::ZeroizeOnDrop; @@ -19,12 +20,23 @@ use pretty_hex::PrettyHex; use core::mem::discriminant; +use digest::Digest; + +#[cfg(feature = "rsa")] +use rsa::signature::{DigestVerifier, DigestSigner}; +#[cfg(feature = "rsa")] +use packets::RSAPubKey; + +// #[cfg(feature = "rsa")] +// use rsa::{PublicKey, RsaPrivateKey, RsaPublicKey, PaddingScheme}; + // RSA requires alloc. #[derive(Debug, Clone, Copy)] pub enum SigType { Ed25519, - RSA256, + #[cfg(feature = "rsa")] + RSA, // Ecdsa } @@ -33,7 +45,8 @@ impl SigType { pub fn from_name(name: &'static str) -> Result<Self> { match name { SSH_NAME_ED25519 => Ok(SigType::Ed25519), - SSH_NAME_RSA_SHA256 => Ok(SigType::RSA256), + #[cfg(feature = "rsa")] + SSH_NAME_RSA_SHA256 => Ok(SigType::RSA), _ => Err(Error::bug()), } } @@ -42,7 +55,8 @@ impl SigType { pub fn algorithm_name(&self) -> &'static str { match self { SigType::Ed25519 => SSH_NAME_ED25519, - SigType::RSA256 => SSH_NAME_RSA_SHA256, + #[cfg(feature = "rsa")] + SigType::RSA => SSH_NAME_RSA_SHA256, } } @@ -75,21 +89,19 @@ impl SigType { .map_err(|_| Error::BadSig) } - (SigType::RSA256, PubKey::RSA(_k), Signature::RSA256(_s)) => { - // TODO - warn!("RSA256 is not implemented"); - Err(Error::BadSig) - // // untested - // use rsa::{PublicKey, RsaPrivateKey, RsaPublicKey, PaddingScheme}; - // let k: RsaPublicKey = k.try_into()?; - // let h = sha2::Sha256::digest(message); - // k.verify(rsa::padding::PaddingScheme::PKCS1v15Sign{ hash: rsa::hash::Hash::SHA2_256}, - // &h, - // s.sig.0) - // .map_err(|e| { - // trace!("RSA signature failed: {e}"); - // Error::BadSig - // }) + #[cfg(feature = "rsa")] + (SigType::RSA, PubKey::RSA(k), Signature::RSA(s)) => { + let verifying_key = rsa::pkcs1v15::VerifyingKey::<sha2::Sha256>::new_with_prefix(k.key.clone()); + let s: Box<[u8]> = s.sig.0.into(); + let signature = s.into(); + + let mut h = sha2::Sha256::new(); + sshwire::hash_ser(&mut h, msg, parse_ctx)?; + verifying_key.verify_digest(h, &signature) + .map_err(|e| { + trace!("RSA signature failed: {e}"); + Error::BadSig + }) } _ => { @@ -107,7 +119,8 @@ pub enum OwnedSig { // salty::Signature doesn't let us borrow the inner bytes, // so we just store raw bytes here. Ed25519([u8; 64]), - _RSA256, // TODO + #[cfg(feature = "rsa")] + RSA(rsa::pkcs1v15::Signature), } impl From<salty::Signature> for OwnedSig { @@ -116,6 +129,13 @@ impl From<salty::Signature> for OwnedSig { } } +#[cfg(feature = "rsa")] +impl From<rsa::pkcs1v15::Signature> for OwnedSig { + fn from(s: rsa::pkcs1v15::Signature) -> Self { + OwnedSig::RSA(s) + } +} + impl TryFrom<Signature<'_>> for OwnedSig { type Error = Error; fn try_from(s: Signature) -> Result<Self> { @@ -124,9 +144,10 @@ impl TryFrom<Signature<'_>> for OwnedSig { let s: [u8; 64] = s.sig.0.try_into().map_err(|_| Error::BadSig)?; Ok(OwnedSig::Ed25519(s)) } - Signature::RSA256(_s) => { - warn!("RSA256 is not implemented"); - Err(Error::BadSig) + #[cfg(feature = "rsa")] + Signature::RSA(s) => { + let s: Box<[u8]> = s.sig.0.into(); + Ok(OwnedSig::RSA(s.into())) } Signature::Unknown(u) => { debug!("Unknown {u} signature"); @@ -139,6 +160,8 @@ impl TryFrom<Signature<'_>> for OwnedSig { #[derive(Debug, Clone, Copy)] pub enum KeyType { Ed25519, + #[cfg(feature = "rsa")] + RSA, } /// A SSH signing key. This may hold the private part locally @@ -151,27 +174,63 @@ pub enum SignKey { #[zeroize(skip)] AgentEd25519(salty::PublicKey), + + #[cfg(feature = "rsa")] + // TODO zeroize doesn't seem supported? though BigUint has Zeroize + #[zeroize(skip)] + RSA(rsa::RsaPrivateKey), + + #[cfg(feature = "rsa")] + #[zeroize(skip)] + AgentRSA(rsa::RsaPublicKey), } impl SignKey { - pub fn generate(ty: KeyType) -> Result<Self> { + pub fn generate(ty: KeyType, bits: Option<usize>) -> Result<Self> { match ty { KeyType::Ed25519 => { + if bits.unwrap_or(256) != 256 { + return Err(Error::msg("Bad key size")); + } let mut seed = [0u8; 32]; random::fill_random(seed.as_mut_slice())?; Ok(Self::Ed25519((&seed).into())) }, + + #[cfg(feature = "rsa")] + KeyType::RSA => { + let bits = bits.unwrap_or(config::RSA_DEFAULT_KEYSIZE); + if bits < config::RSA_MIN_KEYSIZE + || bits > rsa::RsaPublicKey::MAX_SIZE + || (bits % 8 != 0) { + return Err(Error::msg("Bad key size")); + } + + let k = rsa::RsaPrivateKey::new(&mut rand_core::OsRng, bits) + .map_err(|e| { + debug!("RSA key generation error {e}"); + // RNG shouldn't fail, keysize has been checked + Error::bug() + })?; + Ok(Self::RSA(k)) + }, } } pub fn pubkey(&self) -> PubKey { match self { - SignKey::Ed25519(k) => {PubKey::Ed25519(Ed25519PubKey - { key: BinString(k.public.as_bytes()) } ) - } - SignKey::AgentEd25519(pk) => {PubKey::Ed25519(Ed25519PubKey - { key: BinString(pk.as_bytes()) } ) - } + SignKey::Ed25519(k) => PubKey::Ed25519(Ed25519PubKey + { key: BinString(k.public.as_bytes()) } ), + + SignKey::AgentEd25519(pk) => PubKey::Ed25519(Ed25519PubKey + { key: BinString(pk.as_bytes()) } ), + + + #[cfg(feature = "rsa")] + SignKey::RSA(k) => PubKey::RSA(RSAPubKey { key: k.deref().clone() }), + + #[cfg(feature = "rsa")] + SignKey::AgentRSA(pk) => PubKey::RSA(RSAPubKey { key: pk.clone() }), } } @@ -191,7 +250,11 @@ impl SignKey { let k: salty::PublicKey = k.try_into().map_err(|_| Error::BadKey)?; Ok(Self::AgentEd25519(k)) }, - _ => { + + #[cfg (feature = "rsa")] + PubKey::RSA(k) => Ok(Self::AgentRSA(k.key.clone())), + + PubKey::Unknown(_) => { Err(Error::msg("Unsupported agent key")) } } @@ -203,23 +266,42 @@ impl SignKey { | SignKey::Ed25519(_) | SignKey::AgentEd25519(_) => matches!(sig_type, SigType::Ed25519), + + #[cfg(feature = "rsa")] + | SignKey::RSA(_) + | SignKey::AgentRSA(_) + => matches!(sig_type, SigType::RSA), } } pub(crate) fn sign(&self, msg: &impl SSHEncode, parse_ctx: Option<&ParseContext>) -> Result<OwnedSig> { let sig: OwnedSig = match self { SignKey::Ed25519(k) => { - k.sign_parts(|h| { + let sig = k.sign_parts(|h| { sshwire::hash_ser(h, msg, parse_ctx).map_err(|_| salty::Error::ContextTooLong) }) - .trap() - .map(|s| s.into()) - }, - SignKey::AgentEd25519(_) => { - // callers should check for agent keys first - return Error::bug_msg("agent sign") + .trap()?; + sig.into() + } + + #[cfg(feature = "rsa")] + SignKey::RSA(k) => { + let signing_key = rsa::pkcs1v15::SigningKey::<sha2::Sha256>::new_with_prefix(k.clone()); + let mut h = sha2::Sha256::new(); + sshwire::hash_ser(&mut h, msg, parse_ctx)?; + let sig = signing_key.try_sign_digest(h).map_err(|e| { + trace!("RSA signing failed: {e:?}"); + Error::bug() + })?; + sig.into() } - }?; + + // callers should check for agent keys first + | SignKey::AgentEd25519(_) => return Error::bug_msg("agent sign"), + #[cfg(feature = "rsa")] + | SignKey::AgentRSA(_) => return Error::bug_msg("agent sign"), + + }; { // Faults in signing can expose the private key. We verify the signature @@ -236,15 +318,27 @@ impl SignKey { pub(crate) fn is_agent(&self) -> bool { match self { SignKey::Ed25519(_) => false, + #[cfg(feature = "rsa")] + SignKey::RSA(_) => false, + SignKey::AgentEd25519(_) => true, + #[cfg(feature = "rsa")] + SignKey::AgentRSA(_) => true, } } } impl core::fmt::Debug for SignKey { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("SignKey") - .finish() + let s = match self { + Self::Ed25519(_) => "Ed25519", + Self::AgentEd25519(_) => "AgentEd25519", + #[cfg(feature = "rsa")] + Self::RSA(_) => "RSA", + #[cfg(feature = "rsa")] + Self::AgentRSA(_) => "AgentRSA", + }; + write!(f, "SignKey::{s}") } } @@ -260,6 +354,21 @@ impl TryFrom<ssh_key::PrivateKey> for SignKey { }; Ok(SignKey::Ed25519(key)) } + + #[cfg(feature = "rsa")] + ssh_key::private::KeypairData::Rsa(k) => { + let primes = vec![ + (&k.private.p).try_into().map_err(|_| Error::BadKey)?, + (&k.private.q).try_into().map_err(|_| Error::BadKey)?, + ]; + let key = rsa::RsaPrivateKey::from_components( + (&k.public.n).try_into().map_err(|_| Error::BadKey)?, + (&k.public.e).try_into().map_err(|_| Error::BadKey)?, + (&k.private.d).try_into().map_err(|_| Error::BadKey)?, + primes, + ).map_err(|_| Error::BadKey)?; + Ok(SignKey::RSA(key)) + } _ => Err(Error::NotAvailable { what: k.algorithm().as_str() }) } } diff --git a/src/sshwire.rs b/src/sshwire.rs index 70b89b98c127fbb1b5d9c60f76d0953023a9f11c..ab57217fbf0ea4361d05dc2899373e65509166f4 100644 --- a/src/sshwire.rs +++ b/src/sshwire.rs @@ -91,6 +91,8 @@ pub enum WireError { SSHProtoError, + BadKeyFormat, + UnknownPacket { number: u8 }, } @@ -103,6 +105,7 @@ impl From<WireError> for Error { WireError::BadName => Error::BadName, WireError::SSHProtoError => Error::SSHProtoError, WireError::PacketWrong => Error::PacketWrong, + WireError::BadKeyFormat => Error::BadKeyFormat, WireError::UnknownVariant => Error::bug_err_msg("Can't encode Unknown"), WireError::UnknownPacket { number } => Error::UnknownPacket { number }, } @@ -583,6 +586,41 @@ impl DigestUpdate for sha2::Sha256 { } } +fn top_bit_set(b: &[u8]) -> bool { + b.first().unwrap_or(&0) & 0x80 != 0 +} + +#[cfg(feature = "rsa")] +impl SSHEncode for rsa::BigUint { + fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { + let b = self.to_bytes_be(); + let b = b.as_slice(); + + // rfc4251 mpint, need a leading zero byte if top bit is set + let pad = top_bit_set(b); + let len = b.len() as u32 + pad as u32; + len.enc(s)?; + + if pad { + 0u8.enc(s)?; + } + + b.enc(s) + } +} + +#[cfg(feature = "rsa")] +impl<'de> SSHDecode<'de> for rsa::BigUint { + fn dec<S>(s: &mut S) -> WireResult<Self> + where S: SSHSource<'de> { + let b = BinString::dec(s)?; + if top_bit_set(b.0) { + trace!("received negative mpint"); + return Err(WireError::BadKeyFormat) + } + Ok(rsa::BigUint::from_bytes_be(b.0)) + } +} #[cfg(test)] pub(crate) mod tests { diff --git a/sshwire-derive/src/lib.rs b/sshwire-derive/src/lib.rs index db8039fd79b44fe071b53a17f8c87ab0362efa43..e509c8e43caca4d38a6b71d9376a68b6b2898c55 100644 --- a/sshwire-derive/src/lib.rs +++ b/sshwire-derive/src/lib.rs @@ -541,7 +541,7 @@ fn decode_enum_names( if atts.iter().any(|a| matches!(a, FieldAtt::CaptureUnknown)) { // create the Unknown fallthrough but it will be at the end of the match list let mut m = StreamBuilder::new(); - m.push_parsed(format!("_ => {{ s.ctx().seen_unknown = true; Self::{}(Unknown(variant))}}", var.name))?; + m.push_parsed(format!("_ => {{ s.ctx().seen_unknown = true; Self::{}(Unknown::new(variant))}}", var.name))?; if unknown_arm.replace(m).is_some() { return Err(Error::Custom { error: "only one variant can have #[sshwire(unknown)]".into(), span: None}) }