Skip to content

Commit

Permalink
Implement ecdsa-sha2-nistp{256,384,521} (#267)
Browse files Browse the repository at this point in the history
* Moved ECDSA into `ec` module.
* Added support and tests for these algorithms.

Ported and cleaned up from a different fork of thrussh by me.

---------

Co-authored-by: Eugene <inbox@null.page>
  • Loading branch information
robertabcd and Eugeny committed Apr 22, 2024
1 parent e745827 commit 3041b0c
Show file tree
Hide file tree
Showing 12 changed files with 891 additions and 187 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ This is a fork of [Thrussh](https://nest.pijul.com/pijul/thrussh) by Pierre-Éti
* `rsa-sha2-256`
* `rsa-sha2-512`
* `ssh-rsa`
* `ecdsa-sha2-nistp256`
* `ecdsa-sha2-nistp384`
* `ecdsa-sha2-nistp521`
* Dependency updates
* OpenSSH keepalive request handling ✨
* OpenSSH agent forwarding channels ✨
Expand Down
15 changes: 8 additions & 7 deletions russh-keys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ include = [
"src/agent/client.rs",
"src/bcrypt_pbkdf.rs",
"src/blowfish.rs",
"src/ec.rs",
"src/encoding.rs",
"src/format/mod.rs",
"src/format/openssh.rs",
Expand All @@ -39,7 +40,9 @@ block-padding = { version = "0.3", features = ["std"] }
byteorder = "1.4"
data-encoding = "2.3"
dirs = "5.0"
ecdsa = "0.16"
ed25519-dalek = { version= "2.0", features = ["rand_core"] }
elliptic-curve = "0.13"
futures = "0.3"
hmac = "0.12"
inout = { version = "0.1", features = ["std"] }
Expand All @@ -49,22 +52,19 @@ num-bigint = "0.4"
num-integer = "0.1"
openssl = { version = "0.10", optional = true }
p256 = "0.13"
p384 = "0.13"
p521 = "0.13"
pbkdf2 = "0.11"
rand = "0.7"
rand = "0.8"
rand_core = { version = "0.6.4", features = ["std"] }
russh-cryptovec = { version = "0.7.0", path = "../cryptovec" }
serde = { version = "1.0", features = ["derive"] }
sha1 = "0.10"
sha2 = "0.10"
thiserror = "1.0"
tokio = { version = "1.17.0", features = [
"io-util",
"rt-multi-thread",
"time",
"net",
] }
tokio = { version = "1.17.0", features = ["io-util", "rt-multi-thread", "time", "net"] }
tokio-stream = { version = "0.1", features = ["net"] }
typenum = "1.17"
yasna = { version = "0.5.0", features = ["bit-vec", "num-bigint"] }

[features]
Expand All @@ -73,6 +73,7 @@ vendored-openssl = ["openssl", "openssl/vendored"]
[dev-dependencies]
env_logger = "0.10"
tempdir = "0.3"
tokio = { version = "1.17.0", features = ["test-util", "macros", "process"] }

[package.metadata.docs.rs]
features = ["openssl"]
33 changes: 19 additions & 14 deletions russh-keys/src/agent/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ impl<S: AsyncRead + AsyncWrite + Unpin> AgentClient<S> {
key: &key::KeyPair,
constraints: &[Constraint],
) -> Result<(), Error> {
// See IETF draft-miller-ssh-agent-13, section 3.2 for format.
// https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent
self.buf.clear();
self.buf.resize(4);
if constraints.is_empty() {
Expand Down Expand Up @@ -138,6 +140,14 @@ impl<S: AsyncRead + AsyncWrite + Unpin> AgentClient<S> {
self.buf.extend_ssh_mpint(&key.q().unwrap().to_vec());
self.buf.extend_ssh_string(b"");
}
key::KeyPair::EC { ref key } => {
self.buf.extend_ssh_string(key.algorithm().as_bytes());
self.buf.extend_ssh_string(key.ident().as_bytes());
self.buf
.extend_ssh_string(&key.to_public_key().to_sec1_bytes());
self.buf.extend_ssh_mpint(&key.to_secret_bytes());
self.buf.extend_ssh_string(b""); // comment
}
}
if !constraints.is_empty() {
for cons in constraints {
Expand Down Expand Up @@ -275,21 +285,16 @@ impl<S: AsyncRead + AsyncWrite + Unpin> AgentClient<S> {
b"ssh-ed25519" => keys.push(PublicKey::Ed25519(
ed25519_dalek::VerifyingKey::try_from(r.read_string()?)?,
)),
b"ecdsa-sha2-nistp256" => {
let curve = r.read_string()?;
if curve != b"nistp256" {
return Err(Error::EcdsaKeyError(p256::elliptic_curve::Error));
}
let key = r.read_string()?;
keys.push(PublicKey::P256(p256::PublicKey::from_sec1_bytes(key)?));
}
b"ecdsa-sha2-nistp512" => {
crate::KEYTYPE_ECDSA_SHA2_NISTP256
| crate::KEYTYPE_ECDSA_SHA2_NISTP384
| crate::KEYTYPE_ECDSA_SHA2_NISTP521 => {
let curve = r.read_string()?;
if curve != b"nistp521" {
return Err(Error::EcdsaKeyError(p521::elliptic_curve::Error));
let sec1_bytes = r.read_string()?;
let key = crate::ec::PublicKey::from_sec1_bytes(t, sec1_bytes)?;
if curve != key.ident().as_bytes() {
return Err(Error::CouldNotReadKey);
}
let key = r.read_string()?;
keys.push(PublicKey::P521(p521::PublicKey::from_sec1_bytes(key)?));
keys.push(PublicKey::EC { key })
}
t => {
info!("Unsupported key type: {:?}", std::str::from_utf8(t))
Expand Down Expand Up @@ -550,7 +555,7 @@ fn key_blob(public: &key::PublicKey, buf: &mut CryptoVec) -> Result<(), Error> {
#[allow(clippy::indexing_slicing)] // length is known
BigEndian::write_u32(&mut buf[5..], (len1 - len0) as u32);
}
PublicKey::P256(_) | PublicKey::P521(_) => {
PublicKey::EC { .. } => {
buf.extend_ssh_string(&public.public_key_bytes());
}
}
Expand Down
262 changes: 262 additions & 0 deletions russh-keys/src/ec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
use crate::key::safe_rng;
use crate::Error;
use elliptic_curve::{Curve, CurveArithmetic, FieldBytes, FieldBytesSize};

// p521::{SigningKey, VerifyingKey} are wrapped versions and do not provide PartialEq and Eq, hence
// we make our own type alias here.
mod local_p521 {
use rand_core::CryptoRngCore;
use sha2::{Digest, Sha512};

pub type NistP521 = p521::NistP521;
pub type VerifyingKey = ecdsa::VerifyingKey<NistP521>;
pub type SigningKey = ecdsa::SigningKey<NistP521>;
pub type Signature = ecdsa::Signature<NistP521>;
pub type Result<T> = ecdsa::Result<T>;

// Implement signing because p521::NistP521 does not implement DigestPrimitive trait.
pub fn try_sign_with_rng(
key: &SigningKey,
rng: &mut impl CryptoRngCore,
msg: &[u8],
) -> Result<Signature> {
use ecdsa::hazmat::{bits2field, sign_prehashed};
use elliptic_curve::Field;
let prehash = Sha512::digest(msg);
let z = bits2field::<NistP521>(&prehash)?;
let k = p521::Scalar::random(rng);
sign_prehashed(key.as_nonzero_scalar().as_ref(), k, &z).map(|sig| sig.0)
}

// Implement verifying because ecdsa::VerifyingKey<p521::NistP521> does not satisfy the trait
// bound requirements of the DigestVerifier's implementation in ecdsa crate.
pub fn verify(key: &VerifyingKey, msg: &[u8], signature: &Signature) -> Result<()> {
use ecdsa::signature::hazmat::PrehashVerifier;
key.verify_prehash(&Sha512::digest(msg), signature)
}
}

const CURVE_NISTP256: &str = "nistp256";
const CURVE_NISTP384: &str = "nistp384";
const CURVE_NISTP521: &str = "nistp521";

/// An ECC public key.
#[derive(Clone, Eq, PartialEq)]
pub enum PublicKey {
P256(p256::ecdsa::VerifyingKey),
P384(p384::ecdsa::VerifyingKey),
P521(local_p521::VerifyingKey),
}

impl PublicKey {
/// Returns the elliptic curve domain parameter identifiers defined in RFC 5656 section 6.1.
pub fn ident(&self) -> &'static str {
match self {
Self::P256(_) => CURVE_NISTP256,
Self::P384(_) => CURVE_NISTP384,
Self::P521(_) => CURVE_NISTP521,
}
}

/// Returns the ECC public key algorithm name defined in RFC 5656 section 6.2, in the form of
/// `"ecdsa-sha2-[identifier]"`.
pub fn algorithm(&self) -> &'static str {
match self {
Self::P256(_) => crate::ECDSA_SHA2_NISTP256,
Self::P384(_) => crate::ECDSA_SHA2_NISTP384,
Self::P521(_) => crate::ECDSA_SHA2_NISTP521,
}
}

/// Creates a `PrivateKey` from algorithm name and SEC1-encoded point on curve.
pub fn from_sec1_bytes(algorithm: &[u8], bytes: &[u8]) -> Result<Self, Error> {
match algorithm {
crate::KEYTYPE_ECDSA_SHA2_NISTP256 => Ok(Self::P256(
p256::ecdsa::VerifyingKey::from_sec1_bytes(bytes)?,
)),
crate::KEYTYPE_ECDSA_SHA2_NISTP384 => Ok(Self::P384(
p384::ecdsa::VerifyingKey::from_sec1_bytes(bytes)?,
)),
crate::KEYTYPE_ECDSA_SHA2_NISTP521 => Ok(Self::P521(
local_p521::VerifyingKey::from_sec1_bytes(bytes)?,
)),
_ => Err(Error::UnsupportedKeyType {
key_type_string: String::from_utf8(algorithm.to_vec())
.unwrap_or_else(|_| format!("{algorithm:?}")),
key_type_raw: algorithm.to_vec(),
}),
}
}

/// Returns the SEC1-encoded public curve point.
pub fn to_sec1_bytes(&self) -> Vec<u8> {
match self {
Self::P256(key) => key.to_encoded_point(false).as_bytes().to_vec(),
Self::P384(key) => key.to_encoded_point(false).as_bytes().to_vec(),
Self::P521(key) => key.to_encoded_point(false).as_bytes().to_vec(),
}
}

/// Verifies message against signature `(r, s)` using the associated digest algorithm.
pub fn verify(&self, msg: &[u8], r: &[u8], s: &[u8]) -> Result<(), Error> {
use ecdsa::signature::Verifier;
match self {
Self::P256(key) => {
key.verify(msg, &signature_from_scalar_bytes::<p256::NistP256>(r, s)?)
}
Self::P384(key) => {
key.verify(msg, &signature_from_scalar_bytes::<p384::NistP384>(r, s)?)
}
Self::P521(key) => local_p521::verify(
key,
msg,
&signature_from_scalar_bytes::<p521::NistP521>(r, s)?,
),
}
.map_err(Error::from)
}
}

impl std::fmt::Debug for PublicKey {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
Self::P256(_) => write!(f, "P256"),
Self::P384(_) => write!(f, "P384"),
Self::P521(_) => write!(f, "P521"),
}
}
}

/// An ECC private key.
#[derive(Clone, Eq, PartialEq)]
pub enum PrivateKey {
P256(p256::ecdsa::SigningKey),
P384(p384::ecdsa::SigningKey),
P521(local_p521::SigningKey),
}

impl PrivateKey {
/// Creates a `PrivateKey` with algorithm name and scalar.
pub fn new_from_secret_scalar(algorithm: &[u8], scalar: &[u8]) -> Result<Self, Error> {
match algorithm {
crate::KEYTYPE_ECDSA_SHA2_NISTP256 => {
Ok(Self::P256(p256::ecdsa::SigningKey::from_slice(scalar)?))
}
crate::KEYTYPE_ECDSA_SHA2_NISTP384 => {
Ok(Self::P384(p384::ecdsa::SigningKey::from_slice(scalar)?))
}
crate::KEYTYPE_ECDSA_SHA2_NISTP521 => {
Ok(Self::P521(local_p521::SigningKey::from_slice(scalar)?))
}
_ => Err(Error::UnsupportedKeyType {
key_type_string: String::from_utf8(algorithm.to_vec())
.unwrap_or_else(|_| format!("{algorithm:?}")),
key_type_raw: algorithm.to_vec(),
}),
}
}

/// Returns the elliptic curve domain parameter identifiers defined in RFC 5656 section 6.1.
pub fn ident(&self) -> &'static str {
match self {
Self::P256(_) => CURVE_NISTP256,
Self::P384(_) => CURVE_NISTP384,
Self::P521(_) => CURVE_NISTP521,
}
}

/// Returns the ECC public key algorithm name defined in RFC 5656 section 6.2, in the form of
/// `"ecdsa-sha2-[identifier]"`.
pub fn algorithm(&self) -> &'static str {
match self {
Self::P256(_) => crate::ECDSA_SHA2_NISTP256,
Self::P384(_) => crate::ECDSA_SHA2_NISTP384,
Self::P521(_) => crate::ECDSA_SHA2_NISTP521,
}
}

/// Returns the public key.
pub fn to_public_key(&self) -> PublicKey {
match self {
Self::P256(key) => PublicKey::P256(*key.verifying_key()),
Self::P384(key) => PublicKey::P384(*key.verifying_key()),
Self::P521(key) => PublicKey::P521(*key.verifying_key()),
}
}

/// Returns the secret scalar in bytes.
pub fn to_secret_bytes(&self) -> Vec<u8> {
match self {
Self::P256(key) => key.to_bytes().to_vec(),
Self::P384(key) => key.to_bytes().to_vec(),
Self::P521(key) => key.to_bytes().to_vec(),
}
}

/// Sign the message with associated digest algorithm.
pub fn try_sign(&self, msg: &[u8]) -> Result<(Vec<u8>, Vec<u8>), Error> {
use ecdsa::signature::RandomizedSigner;
Ok(match self {
Self::P256(key) => {
signature_to_scalar_bytes(key.try_sign_with_rng(&mut safe_rng(), msg)?)
}
Self::P384(key) => {
signature_to_scalar_bytes(key.try_sign_with_rng(&mut safe_rng(), msg)?)
}
Self::P521(key) => {
signature_to_scalar_bytes(local_p521::try_sign_with_rng(key, &mut safe_rng(), msg)?)
}
})
}
}

impl std::fmt::Debug for PrivateKey {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
Self::P256(_) => write!(f, "P256 {{ (hidden) }}"),
Self::P384(_) => write!(f, "P384 {{ (hidden) }}"),
Self::P521(_) => write!(f, "P521 {{ (hidden) }}"),
}
}
}

fn try_field_bytes_from_mpint<C>(b: &[u8]) -> Option<FieldBytes<C>>
where
C: Curve + CurveArithmetic,
{
use typenum::Unsigned;
let size = FieldBytesSize::<C>::to_usize();
assert!(size > 0);
#[allow(clippy::indexing_slicing)] // Length checked
if b.len() == size + 1 && b[0] == 0 {
Some(FieldBytes::<C>::clone_from_slice(&b[1..]))
} else if b.len() == size {
Some(FieldBytes::<C>::clone_from_slice(b))
} else if b.len() < size {
let mut fb: FieldBytes<C> = Default::default();
fb.as_mut_slice()[size - b.len()..].clone_from_slice(b);
Some(fb)
} else {
None
}
}

fn signature_from_scalar_bytes<C>(r: &[u8], s: &[u8]) -> Result<ecdsa::Signature<C>, Error>
where
C: Curve + CurveArithmetic + elliptic_curve::PrimeCurve,
ecdsa::SignatureSize<C>: elliptic_curve::generic_array::ArrayLength<u8>,
{
Ok(ecdsa::Signature::<C>::from_scalars(
try_field_bytes_from_mpint::<C>(r).ok_or(Error::InvalidSignature)?,
try_field_bytes_from_mpint::<C>(s).ok_or(Error::InvalidSignature)?,
)?)
}

fn signature_to_scalar_bytes<C>(sig: ecdsa::Signature<C>) -> (Vec<u8>, Vec<u8>)
where
C: Curve + CurveArithmetic + elliptic_curve::PrimeCurve,
ecdsa::SignatureSize<C>: elliptic_curve::generic_array::ArrayLength<u8>,
{
let (r, s) = sig.split_bytes();
(r.to_vec(), s.to_vec())
}

0 comments on commit 3041b0c

Please sign in to comment.