Skip to content

Commit

Permalink
Support for NIST P-256 public keys
Browse files Browse the repository at this point in the history
  • Loading branch information
george-hopkins authored and Eugeny committed Nov 5, 2023
1 parent 75ec760 commit 92660ef
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 19 deletions.
1 change: 1 addition & 0 deletions russh-keys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ md5 = "0.7"
num-bigint = "0.4"
num-integer = "0.1"
openssl = { version = "0.10", optional = true }
p256 = "0.13"
pbkdf2 = "0.11"
rand = "0.7"
rand_core = { version = "0.6.4", features = ["std"] }
Expand Down
13 changes: 12 additions & 1 deletion russh-keys/src/agent/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +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::{key, Error};
use crate::{key, Error, PublicKeyBase64};

/// SSH agent client.
pub struct AgentClient<S: AsyncRead + AsyncWrite> {
Expand Down Expand Up @@ -275,6 +275,14 @@ 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::P256KeyError(p256::elliptic_curve::Error));
}
let key = r.read_string()?;
keys.push(PublicKey::P256(p256::PublicKey::from_sec1_bytes(key)?));
}
t => {
info!("Unsupported key type: {:?}", std::str::from_utf8(t))
}
Expand Down Expand Up @@ -534,6 +542,9 @@ 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(_) => {
buf.extend_ssh_string(&public.public_key_bytes());
}
}
Ok(())
}
53 changes: 52 additions & 1 deletion russh-keys/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use std::convert::TryFrom;
use ed25519_dalek::{Signer, Verifier};
#[cfg(feature = "openssl")]
use openssl::pkey::{Private, Public};
use p256::elliptic_curve::generic_array::typenum::Unsigned;
use rand_core::OsRng;
use russh_cryptovec::CryptoVec;
use serde::{Deserialize, Serialize};
Expand All @@ -35,6 +36,8 @@ impl AsRef<str> for Name {
}
}

/// The name of the ecdsa-sha2-nistp256 algorithm for SSH.
pub const ECDSA_SHA2_NISTP256: Name = Name("ecdsa-sha2-nistp256");
/// The name of the Ed25519 algorithm for SSH.
pub const ED25519: Name = Name("ssh-ed25519");
/// The name of the ssh-sha2-512 algorithm for SSH.
Expand All @@ -50,6 +53,7 @@ impl Name {
/// Base name of the private key file for a key name.
pub fn identity_file(&self) -> &'static str {
match *self {
ECDSA_SHA2_NISTP256 => "id_ecdsa",
ED25519 => "id_ed25519",
RSA_SHA2_512 => "id_rsa",
RSA_SHA2_256 => "id_rsa",
Expand Down Expand Up @@ -117,6 +121,8 @@ pub enum PublicKey {
key: OpenSSLPKey,
hash: SignatureHash,
},
#[doc(hidden)]
P256(p256::PublicKey),
}

impl PartialEq for PublicKey {
Expand All @@ -125,7 +131,7 @@ impl PartialEq for PublicKey {
#[cfg(feature = "openssl")]
(Self::RSA { key: a, .. }, Self::RSA { key: b, .. }) => a == b,
(Self::Ed25519(a), Self::Ed25519(b)) => a == b,
#[cfg(feature = "openssl")]
(Self::P256(a), Self::P256(b)) => a == b,
_ => false,
}
}
Expand Down Expand Up @@ -205,6 +211,18 @@ impl PublicKey {
unreachable!()
}
}
b"ecdsa-sha2-nistp256" => {
let mut p = pubkey.reader(0);
let key_algo = p.read_string()?;
let curve = p.read_string()?;
if key_algo != b"ecdsa-sha2-nistp256" || curve != b"nistp256" {
return Err(Error::CouldNotReadKey);
}
let sec1_bytes = p.read_string()?;
let key = p256::PublicKey::from_sec1_bytes(sec1_bytes)
.map_err(|_| Error::CouldNotReadKey)?;
Ok(PublicKey::P256(key))
}
_ => Err(Error::CouldNotReadKey),
}
}
Expand All @@ -215,6 +233,7 @@ impl PublicKey {
PublicKey::Ed25519(_) => ED25519.0,
#[cfg(feature = "openssl")]
PublicKey::RSA { ref hash, .. } => hash.name().0,
PublicKey::P256(_) => ECDSA_SHA2_NISTP256.0,
}
}

Expand All @@ -239,6 +258,30 @@ impl PublicKey {
};
verify().unwrap_or(false)
}
PublicKey::P256(ref public) => {
const FIELD_LEN: usize =
<p256::NistP256 as p256::elliptic_curve::Curve>::FieldBytesSize::USIZE;
let mut reader = sig.reader(0);
let mut read_field = || -> Option<p256::FieldBytes> {
let f = reader.read_mpint().ok()?;
let f = f.strip_prefix(&[0]).unwrap_or(f);
let mut result = [0; FIELD_LEN];
if f.len() > FIELD_LEN {
return None;
}
#[allow(clippy::indexing_slicing)] // length is known
result[FIELD_LEN - f.len()..].copy_from_slice(f);
Some(result.into())
};
let Some(r) = read_field() else { return false };
let Some(s) = read_field() else { return false };
let Ok(signature) = p256::ecdsa::Signature::from_scalars(r, s) else {
return false;
};
p256::ecdsa::VerifyingKey::from(public)
.verify(buffer, &signature)
.is_ok()
}
}
}

Expand Down Expand Up @@ -509,5 +552,13 @@ pub fn parse_public_key(
});
}
}
if t == b"ecdsa-sha2-nistp256" {
if pos.read_string()? != b"nistp256" {
return Err(Error::CouldNotReadKey);
}
let sec1_bytes = pos.read_string()?;
let key = p256::PublicKey::from_sec1_bytes(sec1_bytes)?;
return Ok(PublicKey::P256(key));
}
Err(Error::CouldNotReadKey)
}
19 changes: 18 additions & 1 deletion russh-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ pub enum Error {
/// The type of the key is unsupported
#[error("Invalid Ed25519 key data")]
Ed25519KeyError(#[from] ed25519_dalek::SignatureError),
/// The type of the key is unsupported
#[error("Invalid NIST-P256 key data")]
P256KeyError(#[from] p256::elliptic_curve::Error),
/// The key is encrypted (should supply a password?)
#[error("The key is encrypted")]
KeyIsEncrypted,
Expand Down Expand Up @@ -162,7 +165,7 @@ impl From<yasna::ASN1Error> for Error {
const KEYTYPE_ED25519: &[u8] = b"ssh-ed25519";
const KEYTYPE_RSA: &[u8] = b"ssh-rsa";

/// Load a public key from a file. Ed25519 and RSA keys are supported.
/// Load a public key from a file. Ed25519, EC-DSA and RSA keys are supported.
///
/// ```
/// russh_keys::load_public_key("../files/id_ed25519.pub").unwrap();
Expand Down Expand Up @@ -233,6 +236,12 @@ impl PublicKeyBase64 for key::PublicKey {
#[allow(clippy::unwrap_used)] // TODO check
s.extend_ssh_mpint(&key.0.rsa().unwrap().n().to_vec());
}
key::PublicKey::P256(ref publickey) => {
use encoding::Encoding;
s.extend_ssh_string(b"ecdsa-sha2-nistp256");
s.extend_ssh_string(b"nistp256");
s.extend_ssh_string(&publickey.to_sec1_bytes());
}
}
s
}
Expand Down Expand Up @@ -590,6 +599,14 @@ QR+u0AypRPmzHnOPAAAAEXJvb3RAMTQwOTExNTQ5NDBkAQ==
assert!(check_known_hosts_path(host, port, &hostkey, &path).is_err());
}

#[test]
fn test_parse_p256_public_key() {
env_logger::try_init().unwrap_or(());
let key = "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMxBTpMIGvo7CnordO7wP0QQRqpBwUjOLl4eMhfucfE1sjTYyK5wmTl1UqoSDS1PtRVTBdl+0+9pquFb46U7fwg=";

parse_public_key_base64(key).unwrap();
}

#[test]
#[cfg(feature = "openssl")]
fn test_srhb() {
Expand Down
15 changes: 14 additions & 1 deletion russh-keys/src/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub struct SignatureBytes(pub [u8; 64]);
pub enum Signature {
/// An Ed25519 signature
Ed25519(SignatureBytes),
/// An EC-DSA NIST P-256 signature
P256(Vec<u8>),
/// An RSA signature
RSA { hash: SignatureHash, bytes: Vec<u8> },
}
Expand All @@ -34,6 +36,15 @@ impl Signature {
bytes_.extend_ssh_string(t);
bytes_.extend_ssh_string(&bytes.0[..]);
}
Signature::P256(ref bytes) => {
let t = b"ecdsa-sha2-nistp256";
#[allow(clippy::unwrap_used)] // Vec<>.write_all can't fail
bytes_
.write_u32::<BigEndian>((t.len() + bytes.len() + 8) as u32)
.unwrap();
bytes_.extend_ssh_string(t);
bytes_.extend_ssh_string(bytes);
}
Signature::RSA {
ref hash,
ref bytes,
Expand All @@ -48,7 +59,7 @@ impl Signature {
.write_u32::<BigEndian>((t.len() + bytes.len() + 8) as u32)
.unwrap();
bytes_.extend_ssh_string(t);
bytes_.extend_ssh_string(&bytes[..]);
bytes_.extend_ssh_string(bytes);
}
}
data_encoding::BASE64_NOPAD.encode(&bytes_[..])
Expand Down Expand Up @@ -80,6 +91,7 @@ impl Signature {
hash: SignatureHash::SHA1,
bytes: bytes.to_vec(),
}),
b"ecdsa-sha2-nistp256" => Ok(Signature::P256(bytes.to_vec())),
_ => Err(Error::UnknownSignatureType {
sig_type: std::str::from_utf8(typ).unwrap_or("").to_string(),
}),
Expand All @@ -92,6 +104,7 @@ impl AsRef<[u8]> for Signature {
match *self {
Signature::Ed25519(ref signature) => &signature.0,
Signature::RSA { ref bytes, .. } => &bytes[..],
Signature::P256(ref signature) => signature,
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions russh/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use russh_cryptovec::CryptoVec;
use russh_keys::encoding::*;
use russh_keys::key::*;
use russh_keys::PublicKeyBase64;

#[doc(hidden)]
pub trait PubKey {
Expand All @@ -29,6 +30,9 @@ impl PubKey for PublicKey {
buffer.extend_ssh_string(ED25519.0.as_bytes());
buffer.extend_ssh_string(public.as_bytes());
}
PublicKey::P256(_) => {
buffer.extend_ssh_string(&self.public_key_bytes());
}
#[cfg(feature = "openssl")]
PublicKey::RSA { ref key, .. } => {
#[allow(clippy::unwrap_used)] // type known
Expand Down
27 changes: 12 additions & 15 deletions russh/src/negotiation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,27 +76,24 @@ const HMAC_ORDER: &[mac::Name] = &[
];

impl Preferred {
#[cfg(feature = "openssl")]
pub const DEFAULT: Preferred = Preferred {
kex: SAFE_KEX_ORDER,
key: &[key::ED25519, key::RSA_SHA2_256, key::RSA_SHA2_512],
cipher: CIPHER_ORDER,
mac: HMAC_ORDER,
compression: &["none", "zlib", "zlib@openssh.com"],
};

#[cfg(not(feature = "openssl"))]
pub const DEFAULT: Preferred = Preferred {
kex: SAFE_KEX_ORDER,
key: &[key::ED25519],
key: &[
key::ED25519,
key::ECDSA_SHA2_NISTP256,
#[cfg(feature = "openssl")]
key::RSA_SHA2_256,
#[cfg(feature = "openssl")]
key::RSA_SHA2_512,
],
cipher: CIPHER_ORDER,
mac: HMAC_ORDER,
compression: &["none", "zlib", "zlib@openssh.com"],
};

pub const COMPRESSED: Preferred = Preferred {
kex: SAFE_KEX_ORDER,
key: &[key::ED25519, key::RSA_SHA2_256, key::RSA_SHA2_512],
key: Preferred::DEFAULT.key,
cipher: CIPHER_ORDER,
mac: HMAC_ORDER,
compression: &["zlib", "zlib@openssh.com", "none"],
Expand All @@ -121,15 +118,15 @@ impl Named for () {
}
}

#[cfg(not(feature = "openssl"))]
use russh_keys::key::ED25519;
#[cfg(feature = "openssl")]
use russh_keys::key::{ED25519, SSH_RSA};
use russh_keys::key::SSH_RSA;
use russh_keys::key::{ECDSA_SHA2_NISTP256, ED25519};

impl Named for PublicKey {
fn name(&self) -> &'static str {
match self {
PublicKey::Ed25519(_) => ED25519.0,
PublicKey::P256(_) => ECDSA_SHA2_NISTP256.0,
#[cfg(feature = "openssl")]
PublicKey::RSA { .. } => SSH_RSA.0,
}
Expand Down

0 comments on commit 92660ef

Please sign in to comment.