Skip to content

Commit

Permalink
Add pure-rust RSA implementation (#273)
Browse files Browse the repository at this point in the history
* SSH encoding of RSA keys is moved into `protocol` module.
* Decouple the SSH encoding into traits.
* When `openssl` feature is not enabled, the pure-rust RSA impl is used.

Alternative implementation for
#225
  • Loading branch information
robertabcd committed Apr 28, 2024
1 parent 3041b0c commit c850dbd
Show file tree
Hide file tree
Showing 17 changed files with 712 additions and 411 deletions.
7 changes: 5 additions & 2 deletions russh-keys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ ctr = "0.9"
block-padding = { version = "0.3", features = ["std"] }
byteorder = "1.4"
data-encoding = "2.3"
digest = "0.10"
dirs = "5.0"
ecdsa = "0.16"
ed25519-dalek = { version= "2.0", features = ["rand_core"] }
Expand All @@ -55,12 +56,14 @@ p256 = "0.13"
p384 = "0.13"
p521 = "0.13"
pbkdf2 = "0.11"
pkcs1 = "0.7"
rand = "0.8"
rand_core = { version = "0.6.4", features = ["std"] }
rsa = "0.9"
russh-cryptovec = { version = "0.7.0", path = "../cryptovec" }
serde = { version = "1.0", features = ["derive"] }
sha1 = "0.10"
sha2 = "0.10"
sha1 = { version = "0.10", features = ["oid"] }
sha2 = { version = "0.10", features = ["oid"] }
thiserror = "1.0"
tokio = { version = "1.17.0", features = ["io-util", "rt-multi-thread", "time", "net"] }
tokio-stream = { version = "0.1", features = ["net"] }
Expand Down
43 changes: 8 additions & 35 deletions russh-keys/src/agent/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use super::{msg, Constraint};
use crate::encoding::{Encoding, Reader};
use crate::key::{PublicKey, SignatureHash};
use crate::protocol;
use crate::{key, Error, PublicKeyBase64};

/// SSH agent client.
Expand Down Expand Up @@ -121,24 +122,11 @@ impl<S: AsyncRead + AsyncWrite + Unpin> AgentClient<S> {
self.buf.extend(pair.verifying_key().as_bytes());
self.buf.extend_ssh_string(b"");
}
#[cfg(feature = "openssl")]
#[allow(clippy::unwrap_used)] // key is known to be private
key::KeyPair::RSA { ref key, .. } => {
self.buf.extend_ssh_string(b"ssh-rsa");
self.buf.extend_ssh_mpint(&key.n().to_vec());
self.buf.extend_ssh_mpint(&key.e().to_vec());
self.buf.extend_ssh_mpint(&key.d().to_vec());
if let Some(iqmp) = key.iqmp() {
self.buf.extend_ssh_mpint(&iqmp.to_vec());
} else {
let mut ctx = openssl::bn::BigNumContext::new()?;
let mut iqmp = openssl::bn::BigNum::new()?;
iqmp.mod_inverse(key.p().unwrap(), key.q().unwrap(), &mut ctx)?;
self.buf.extend_ssh_mpint(&iqmp.to_vec());
}
self.buf.extend_ssh_mpint(&key.p().unwrap().to_vec());
self.buf.extend_ssh_mpint(&key.q().unwrap().to_vec());
self.buf.extend_ssh_string(b"");
self.buf
.extend_ssh(&protocol::RsaPrivateKey::try_from(key)?);
}
key::KeyPair::EC { ref key } => {
self.buf.extend_ssh_string(key.algorithm().as_bytes());
Expand Down Expand Up @@ -267,21 +255,10 @@ impl<S: AsyncRead + AsyncWrite + Unpin> AgentClient<S> {
let t = r.read_string()?;
debug!("t = {:?}", std::str::from_utf8(t));
match t {
#[cfg(feature = "openssl")]
b"ssh-rsa" => {
let e = r.read_mpint()?;
let n = r.read_mpint()?;
use openssl::bn::BigNum;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
keys.push(PublicKey::RSA {
key: key::OpenSSLPKey(PKey::from_rsa(Rsa::from_public_components(
BigNum::from_slice(n)?,
BigNum::from_slice(e)?,
)?)?),
hash: SignatureHash::SHA2_512,
})
}
b"ssh-rsa" => keys.push(key::PublicKey::new_rsa_with_hash(
&r.read_ssh()?,
SignatureHash::SHA2_512,
)?),
b"ssh-ed25519" => keys.push(PublicKey::Ed25519(
ed25519_dalek::VerifyingKey::try_from(r.read_string()?)?,
)),
Expand Down Expand Up @@ -351,7 +328,6 @@ impl<S: AsyncRead + AsyncWrite + Unpin> AgentClient<S> {
self.buf.extend_ssh_string(data);
debug!("public = {:?}", public);
let hash = match public {
#[cfg(feature = "openssl")]
PublicKey::RSA { hash, .. } => match hash {
SignatureHash::SHA2_256 => 2,
SignatureHash::SHA2_512 => 4,
Expand Down Expand Up @@ -534,14 +510,11 @@ impl<S: AsyncRead + AsyncWrite + Unpin> AgentClient<S> {

fn key_blob(public: &key::PublicKey, buf: &mut CryptoVec) -> Result<(), Error> {
match *public {
#[cfg(feature = "openssl")]
PublicKey::RSA { ref key, .. } => {
buf.extend(&[0, 0, 0, 0]);
let len0 = buf.len();
buf.extend_ssh_string(b"ssh-rsa");
let rsa = key.0.rsa()?;
buf.extend_ssh_mpint(&rsa.e().to_vec());
buf.extend_ssh_mpint(&rsa.n().to_vec());
buf.extend_ssh(&protocol::RsaPublicKey::from(key));
let len1 = buf.len();
#[allow(clippy::indexing_slicing)] // length is known
BigEndian::write_u32(&mut buf[5..], (len1 - len0) as u32);
Expand Down
60 changes: 13 additions & 47 deletions russh-keys/src/agent/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ use {std, tokio};

use super::{msg, Constraint};
use crate::encoding::{Encoding, Position, Reader};
#[cfg(feature = "openssl")]
use crate::key::SignatureHash;
use crate::{key, Error};
use crate::{key, protocol, Error};

#[derive(Clone)]
#[allow(clippy::type_complexity)]
Expand Down Expand Up @@ -271,55 +270,22 @@ impl<S: AsyncRead + AsyncWrite + Send + Unpin + 'static, A: Agent + Send + Sync
#[allow(clippy::indexing_slicing)] // positions checked before
(self.buf[pos0..pos1].to_vec(), key::KeyPair::Ed25519(secret))
}
#[cfg(feature = "openssl")]
b"ssh-rsa" => {
use openssl::bn::{BigNum, BigNumContext};
use openssl::rsa::Rsa;
let n = r.read_mpint()?;
let e = r.read_mpint()?;
let d = BigNum::from_slice(r.read_mpint()?)?;
let q_inv = r.read_mpint()?;
let p = BigNum::from_slice(r.read_mpint()?)?;
let q = BigNum::from_slice(r.read_mpint()?)?;
let (dp, dq) = {
let one = BigNum::from_u32(1)?;
let p1 = p.as_ref() - one.as_ref();
let q1 = q.as_ref() - one.as_ref();
let mut context = BigNumContext::new()?;
let mut dp = BigNum::new()?;
let mut dq = BigNum::new()?;
dp.checked_rem(&d, &p1, &mut context)?;
dq.checked_rem(&d, &q1, &mut context)?;
(dp, dq)
};
let _comment = r.read_string()?;
let key = Rsa::from_private_components(
BigNum::from_slice(n)?,
BigNum::from_slice(e)?,
d,
p,
q,
dp,
dq,
BigNum::from_slice(q_inv)?,
)?;
let key =
key::KeyPair::new_rsa_with_hash(&r.read_ssh()?, None, SignatureHash::SHA2_256)?;

let len0 = writebuf.len();
writebuf.extend_ssh_string(b"ssh-rsa");
writebuf.extend_ssh_mpint(e);
writebuf.extend_ssh_mpint(n);
let mut blob = Vec::new();
if let key::KeyPair::RSA { ref key, .. } = key {
let public = protocol::RsaPublicKey::from(key);
blob.extend_ssh_string(b"ssh-rsa");
blob.extend_ssh(&public);
} else {
return Err(Error::KeyIsCorrupt);
};

#[allow(clippy::indexing_slicing)] // length is known
let blob = writebuf[len0..].to_vec();
writebuf.resize(len0);
writebuf.push(msg::SUCCESS);
(
blob,
key::KeyPair::RSA {
key,
hash: SignatureHash::SHA2_256,
},
)

(blob, key)
}
_ => return Ok(false),
};
Expand Down
210 changes: 210 additions & 0 deletions russh-keys/src/backend_openssl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
use std::convert::TryFrom;

use crate::key::{RsaCrtExtra, SignatureHash};
use crate::{protocol, Error};
use openssl::{
bn::{BigNum, BigNumContext, BigNumRef},
hash::MessageDigest,
pkey::{PKey, Private, Public},
rsa::Rsa,
};

#[derive(Clone)]
pub struct RsaPublic {
key: Rsa<Public>,
pkey: PKey<Public>,
}

impl RsaPublic {
pub fn verify_detached(&self, hash: &SignatureHash, msg: &[u8], sig: &[u8]) -> bool {
openssl::sign::Verifier::new(message_digest_for(hash), &self.pkey)
.and_then(|mut v| v.verify_oneshot(sig, msg))
.unwrap_or(false)
}
}

impl TryFrom<&protocol::RsaPublicKey<'_>> for RsaPublic {
type Error = Error;

fn try_from(pk: &protocol::RsaPublicKey<'_>) -> Result<Self, Self::Error> {
let key = Rsa::from_public_components(
BigNum::from_slice(&pk.modulus)?,
BigNum::from_slice(&pk.public_exponent)?,
)?;
Ok(Self {
pkey: PKey::from_rsa(key.clone())?,
key,
})
}
}

impl<'a> From<&RsaPublic> for protocol::RsaPublicKey<'a> {
fn from(key: &RsaPublic) -> Self {
Self {
modulus: key.key.n().to_vec().into(),
public_exponent: key.key.e().to_vec().into(),
}
}
}

impl PartialEq for RsaPublic {
fn eq(&self, b: &RsaPublic) -> bool {
self.pkey.public_eq(&b.pkey)
}
}

impl Eq for RsaPublic {}

impl std::fmt::Debug for RsaPublic {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "RsaPublic {{ (hidden) }}")
}
}

#[derive(Clone)]
pub struct RsaPrivate {
key: Rsa<Private>,
pkey: PKey<Private>,
}

impl RsaPrivate {
pub fn new(
sk: &protocol::RsaPrivateKey<'_>,
extra: Option<&RsaCrtExtra<'_>>,
) -> Result<Self, Error> {
let (d, p, q) = (
BigNum::from_slice(&sk.private_exponent)?,
BigNum::from_slice(&sk.prime1)?,
BigNum::from_slice(&sk.prime2)?,
);
let (dp, dq) = if let Some(extra) = extra {
(
BigNum::from_slice(&extra.dp)?,
BigNum::from_slice(&extra.dq)?,
)
} else {
calc_dp_dq(d.as_ref(), p.as_ref(), q.as_ref())?
};
let key = Rsa::from_private_components(
BigNum::from_slice(&sk.public_key.modulus)?,
BigNum::from_slice(&sk.public_key.public_exponent)?,
d,
p,
q,
dp,
dq,
BigNum::from_slice(&sk.coefficient)?,
)?;
key.check_key()?;
Ok(Self {
pkey: PKey::from_rsa(key.clone())?,
key,
})
}

pub fn new_from_der(der: &[u8]) -> Result<Self, Error> {
let key = Rsa::private_key_from_der(der)?;
key.check_key()?;
Ok(Self {
pkey: PKey::from_rsa(key.clone())?,
key,
})
}

pub fn generate(bits: usize) -> Result<Self, Error> {
let key = Rsa::generate(bits as u32)?;
Ok(Self {
pkey: PKey::from_rsa(key.clone())?,
key,
})
}

pub fn sign(&self, hash: &SignatureHash, msg: &[u8]) -> Result<Vec<u8>, Error> {
Ok(
openssl::sign::Signer::new(message_digest_for(hash), &self.pkey)?
.sign_oneshot_to_vec(msg)?,
)
}
}

impl<'a> TryFrom<&RsaPrivate> for protocol::RsaPrivateKey<'a> {
type Error = Error;

fn try_from(key: &RsaPrivate) -> Result<protocol::RsaPrivateKey<'a>, Self::Error> {
let key = &key.key;
// We always set these.
if let (Some(p), Some(q), Some(iqmp)) = (key.p(), key.q(), key.iqmp()) {
Ok(protocol::RsaPrivateKey {
public_key: protocol::RsaPublicKey {
modulus: key.n().to_vec().into(),
public_exponent: key.e().to_vec().into(),
},
private_exponent: key.d().to_vec().into(),
prime1: p.to_vec().into(),
prime2: q.to_vec().into(),
coefficient: iqmp.to_vec().into(),
comment: b"".as_slice().into(),
})
} else {
Err(Error::KeyIsCorrupt)
}
}
}

impl<'a> TryFrom<&RsaPrivate> for RsaCrtExtra<'a> {
type Error = Error;

fn try_from(key: &RsaPrivate) -> Result<RsaCrtExtra<'a>, Self::Error> {
let key = &key.key;
// We always set these.
if let (Some(dp), Some(dq)) = (key.dmp1(), key.dmq1()) {
Ok(RsaCrtExtra {
dp: dp.to_vec().into(),
dq: dq.to_vec().into(),
})
} else {
Err(Error::KeyIsCorrupt)
}
}
}

impl<'a> From<&RsaPrivate> for protocol::RsaPublicKey<'a> {
fn from(key: &RsaPrivate) -> Self {
Self {
modulus: key.key.n().to_vec().into(),
public_exponent: key.key.e().to_vec().into(),
}
}
}

impl TryFrom<&RsaPrivate> for RsaPublic {
type Error = Error;

fn try_from(key: &RsaPrivate) -> Result<Self, Self::Error> {
let key = Rsa::from_public_components(key.key.n().to_owned()?, key.key.e().to_owned()?)?;
Ok(Self {
pkey: PKey::from_rsa(key.clone())?,
key,
})
}
}

fn message_digest_for(hash: &SignatureHash) -> MessageDigest {
match hash {
SignatureHash::SHA2_256 => MessageDigest::sha256(),
SignatureHash::SHA2_512 => MessageDigest::sha512(),
SignatureHash::SHA1 => MessageDigest::sha1(),
}
}

fn calc_dp_dq(d: &BigNumRef, p: &BigNumRef, q: &BigNumRef) -> Result<(BigNum, BigNum), Error> {
let one = BigNum::from_u32(1)?;
let p1 = p - one.as_ref();
let q1 = q - one.as_ref();
let mut context = BigNumContext::new()?;
let mut dp = BigNum::new()?;
let mut dq = BigNum::new()?;
dp.checked_rem(d, &p1, &mut context)?;
dq.checked_rem(d, &q1, &mut context)?;
Ok((dp, dq))
}

0 comments on commit c850dbd

Please sign in to comment.