Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for NIST P-256 public keys #209

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ This is a fork of [Thrussh](https://nest.pijul.com/pijul/thrussh) by Pierre-Éti
* `[email protected]` ✨
* `[email protected]` ✨
* `[email protected]` ✨
* Host keys:
* Key types:
* `ssh-ed25519`
* `rsa-sha2-256`
* `rsa-sha2-512`
* `ssh-rsa` ✨
* `ecdsa-sha2-nistp256` ✨
* Dependency updates
* OpenSSH keepalive request handling ✨
* OpenSSH agent forwarding channels ✨
Expand Down
1 change: 1 addition & 0 deletions russh-keys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ block-padding = { version = "0.3", features = ["std"] }
byteorder = "1.4"
data-encoding = "2.3"
dirs = "5.0"
ecdsa = { version = "0.16", features = ["serde"] }
ed25519-dalek = { version= "2.0", features = ["rand_core"] }
futures = "0.3"
hmac = "0.12"
Expand Down
3 changes: 3 additions & 0 deletions russh-keys/src/agent/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ impl<S: AsyncRead + AsyncWrite + Unpin> AgentClient<S> {
self.buf.extend(pair.verifying_key().as_bytes());
self.buf.extend_ssh_string(b"");
}
key::KeyPair::EcdsaSha2NistP256(ref key) => {
todo!("TODO");
}
#[cfg(feature = "openssl")]
#[allow(clippy::unwrap_used)] // key is known to be private
key::KeyPair::RSA { ref key, .. } => {
Expand Down
3 changes: 3 additions & 0 deletions russh-keys/src/deps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub use ed25519_dalek;
pub use ecdsa;
pub use p256;
21 changes: 20 additions & 1 deletion russh-keys/src/format/openssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use ctr::Ctr64BE;
use openssl::bn::BigNum;

use crate::encoding::Reader;
use crate::{key, Error, KEYTYPE_ED25519, KEYTYPE_RSA};
use crate::{key, Error, KEYTYPE_ED25519, KEYTYPE_P256, KEYTYPE_RSA};

/// Decode a secret key given in the OpenSSH format, deciphering it if
/// needed using the supplied password.
Expand Down Expand Up @@ -79,6 +79,25 @@ pub fn decode_openssh(secret: &[u8], password: Option<&str>) -> Result<key::KeyP
hash: key::SignatureHash::SHA2_512,
});
}
} else if key_type == KEYTYPE_P256 {
let curve = position.read_string()?;
if curve != b"nistp256" {
return Err(Error::P256KeyError(p256::elliptic_curve::Error));
}

let data = position.read_string()?;
let (check, _public_key_bytes) = data.split_at(1);
if check.get(0) != Some(&4) {
return Err(Error::P256KeyError(p256::elliptic_curve::Error));
}

let data = position.read_string()?;
let (_check, private_key_bytes) = data.split_at(1);

let field = p256::FieldBytes::from_slice(private_key_bytes);
return p256::SecretKey::from_bytes(field)
.map_err(|e| Error::P256KeyError(e))
.map(|k| key::KeyPair::EcdsaSha2NistP256(k));
} else {
return Err(Error::UnsupportedKeyType {
key_type_string: String::from_utf8(key_type.to_vec())
Expand Down
2 changes: 2 additions & 0 deletions russh-keys/src/format/pkcs8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ fn test_read_write_pkcs8() {
let key = decode_pkcs8(&ciphertext, Some(password)).unwrap();
match key {
key::KeyPair::Ed25519 { .. } => println!("Ed25519"),
key::KeyPair::EcdsaSha2NistP256(_) => println!("P256"),
#[cfg(feature = "openssl")]
key::KeyPair::RSA { .. } => println!("RSA"),
}
Expand Down Expand Up @@ -316,6 +317,7 @@ pub fn encode_pkcs8(key: &key::KeyPair) -> Vec<u8> {
yasna::construct_der(|writer| {
writer.write_sequence(|writer| match *key {
key::KeyPair::Ed25519(ref pair) => write_key_v1(writer, pair),
key::KeyPair::EcdsaSha2NistP256(_) => todo!("TODO"),
#[cfg(feature = "openssl")]
key::KeyPair::RSA { ref key, .. } => write_key_v0(writer, key),
})
Expand Down
56 changes: 53 additions & 3 deletions russh-keys/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
//
use std::convert::TryFrom;

use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use ecdsa::elliptic_curve::ff::PrimeField;
use ed25519_dalek::{Signer, Verifier};
#[cfg(feature = "openssl")]
use openssl::pkey::{Private, Public};
Expand All @@ -22,7 +24,7 @@ use rand_core::OsRng;
use russh_cryptovec::CryptoVec;
use serde::{Deserialize, Serialize};

use crate::encoding::{Encoding, Reader};
use crate::encoding::{mpint_len, Encoding, Reader};
pub use crate::signature::*;
use crate::Error;

Expand Down Expand Up @@ -215,7 +217,7 @@ impl PublicKey {
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" {
if key_algo != ECDSA_SHA2_NISTP256.0.as_bytes() || curve != b"nistp256" {
return Err(Error::CouldNotReadKey);
}
let sec1_bytes = p.read_string()?;
Expand Down Expand Up @@ -325,6 +327,7 @@ impl Verify for PublicKey {
#[allow(clippy::large_enum_variant)]
pub enum KeyPair {
Ed25519(ed25519_dalek::SigningKey),
EcdsaSha2NistP256(p256::SecretKey),
#[cfg(feature = "openssl")]
RSA {
key: openssl::rsa::Rsa<Private>,
Expand All @@ -339,6 +342,8 @@ impl Clone for KeyPair {
Self::Ed25519(kp) => {
Self::Ed25519(ed25519_dalek::SigningKey::from_bytes(&kp.to_bytes()))
}
#[allow(clippy::expect_used)]
Self::EcdsaSha2NistP256(k) => Self::EcdsaSha2NistP256(k.clone()),
#[cfg(feature = "openssl")]
Self::RSA { key, hash } => Self::RSA {
key: key.clone(),
Expand All @@ -356,6 +361,7 @@ impl std::fmt::Debug for KeyPair {
"Ed25519 {{ public: {:?}, secret: (hidden) }}",
key.verifying_key().as_bytes()
),
KeyPair::EcdsaSha2NistP256 { .. } => write!(f, "P256 {{ (hidden) }}"),
#[cfg(feature = "openssl")]
KeyPair::RSA { .. } => write!(f, "RSA {{ (hidden) }}"),
}
Expand All @@ -373,6 +379,7 @@ impl KeyPair {
pub fn clone_public_key(&self) -> Result<PublicKey, Error> {
Ok(match self {
KeyPair::Ed25519(ref key) => PublicKey::Ed25519(key.verifying_key()),
KeyPair::EcdsaSha2NistP256(ref key) => PublicKey::P256(key.public_key()),
#[cfg(feature = "openssl")]
KeyPair::RSA { ref key, ref hash } => {
use openssl::pkey::PKey;
Expand All @@ -390,6 +397,7 @@ impl KeyPair {
pub fn name(&self) -> &'static str {
match *self {
KeyPair::Ed25519(_) => ED25519.0,
KeyPair::EcdsaSha2NistP256(_) => ECDSA_SHA2_NISTP256.0,
#[cfg(feature = "openssl")]
KeyPair::RSA { ref hash, .. } => hash.name().0,
}
Expand All @@ -414,10 +422,12 @@ impl KeyPair {
/// Sign a slice using this algorithm.
pub fn sign_detached(&self, to_sign: &[u8]) -> Result<Signature, Error> {
match self {
#[allow(clippy::unwrap_used)]
KeyPair::Ed25519(ref secret) => Ok(Signature::Ed25519(SignatureBytes(
secret.sign(to_sign).to_bytes(),
))),
KeyPair::EcdsaSha2NistP256(ref secret) => Ok(Signature::P256({
p256_signature(secret, to_sign)
})),
#[cfg(feature = "openssl")]
KeyPair::RSA { ref key, ref hash } => Ok(Signature::RSA {
bytes: rsa_signature(hash, key, to_sign)?,
Expand All @@ -443,6 +453,10 @@ impl KeyPair {
buffer.extend_ssh_string(ED25519.0.as_bytes());
buffer.extend_ssh_string(signature.to_bytes().as_slice());
}
KeyPair::EcdsaSha2NistP256(ref secret) => {
let signature = p256_signature(secret, to_sign.as_ref());
encode_ecdsa_signature(buffer, &ECDSA_SHA2_NISTP256, &signature);
}
#[cfg(feature = "openssl")]
KeyPair::RSA { ref key, ref hash } => {
// https://tools.ietf.org/html/draft-rsa-dsa-sha2-256-02#section-2.2
Expand All @@ -468,6 +482,10 @@ impl KeyPair {
buffer.extend_ssh_string(ED25519.0.as_bytes());
buffer.extend_ssh_string(signature.to_bytes().as_slice());
}
KeyPair::EcdsaSha2NistP256(ref secret) => {
let signature = p256_signature(secret, buffer.as_ref());
encode_ecdsa_signature(buffer, &ECDSA_SHA2_NISTP256, &signature);
}
#[cfg(feature = "openssl")]
KeyPair::RSA { ref key, ref hash } => {
// https://tools.ietf.org/html/draft-rsa-dsa-sha2-256-02#section-2.2
Expand All @@ -486,6 +504,7 @@ impl KeyPair {
pub fn with_signature_hash(&self, hash: SignatureHash) -> Option<Self> {
match self {
KeyPair::Ed25519(_) => None,
KeyPair::EcdsaSha2NistP256(_) => None,
#[cfg(feature = "openssl")]
KeyPair::RSA { key, .. } => Some(KeyPair::RSA {
key: key.clone(),
Expand Down Expand Up @@ -519,6 +538,37 @@ fn rsa_signature(
Ok(signer.sign_to_vec()?)
}

fn p256_signature(
secret: &p256::SecretKey,
to_sign: &[u8],
) -> p256::ecdsa::Signature {
let signer = p256::ecdsa::SigningKey::from(secret);
signer.sign(to_sign)
}

pub(crate) fn encode_ecdsa_signature<E: Encoding + WriteBytesExt>(
buffer: &mut E,
name: &Name,
signature: &p256::ecdsa::Signature,
) {
let r = signature.r().to_repr();
let s = signature.s().to_repr();

let mut blob = vec![];
blob.extend_ssh_mpint(&r);
blob.extend_ssh_mpint(&s);

#[allow(clippy::unwrap_used)] // Vec<>.write_all can't fail
buffer
// .write_u32::<BigEndian>((name.0.len() + mpint_len(&r) + mpint_len(&s) + 4) as u32)
.write_u32::<BigEndian>((name.0.len() + blob.len() + 8) as u32)
.unwrap();
buffer.extend_ssh_string(name.0.as_bytes());
buffer.extend_ssh_string(&blob[..]);
// buffer.extend_ssh_mpint(&r);
// buffer.extend_ssh_mpint(&s);
}

/// Parse a public key from a byte slice.
pub fn parse_public_key(
p: &[u8],
Expand Down
8 changes: 8 additions & 0 deletions russh-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub mod key;
pub mod signature;

mod format;
pub mod deps;
pub use format::*;

/// A module to write SSH agent.
Expand Down Expand Up @@ -163,6 +164,7 @@ impl From<yasna::ASN1Error> for Error {
}

const KEYTYPE_ED25519: &[u8] = b"ssh-ed25519";
const KEYTYPE_P256: &[u8] = b"ecdsa-sha2-nistp256";
const KEYTYPE_RSA: &[u8] = b"ssh-rsa";

/// Load a public key from a file. Ed25519, EC-DSA and RSA keys are supported.
Expand Down Expand Up @@ -261,6 +263,12 @@ impl PublicKeyBase64 for key::KeyPair {
s.write_u32::<BigEndian>(public.len() as u32).unwrap();
s.extend_from_slice(public.as_slice());
}
key::KeyPair::EcdsaSha2NistP256(ref key) => {
let public = key.public_key().to_sec1_bytes();
#[allow(clippy::unwrap_used)] // Vec<>.write can't fail
s.write_u32::<BigEndian>(public.len() as u32).unwrap();
s.extend_from_slice(&public);
},
#[cfg(feature = "openssl")]
key::KeyPair::RSA { ref key, .. } => {
use encoding::Encoding;
Expand Down
20 changes: 8 additions & 12 deletions russh-keys/src/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeTuple;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::key::SignatureHash;
use crate::key::{SignatureHash, ECDSA_SHA2_NISTP256, encode_ecdsa_signature};
use crate::Error;

pub struct SignatureBytes(pub [u8; 64]);
Expand All @@ -17,7 +17,7 @@ pub enum Signature {
/// An Ed25519 signature
Ed25519(SignatureBytes),
/// An EC-DSA NIST P-256 signature
P256(Vec<u8>),
P256(p256::ecdsa::Signature),
/// An RSA signature
RSA { hash: SignatureHash, bytes: Vec<u8> },
}
Expand All @@ -36,14 +36,8 @@ 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::P256(ref signature) => {
encode_ecdsa_signature(&mut bytes_, &ECDSA_SHA2_NISTP256, &signature);
}
Signature::RSA {
ref hash,
Expand Down Expand Up @@ -91,7 +85,9 @@ impl Signature {
hash: SignatureHash::SHA1,
bytes: bytes.to_vec(),
}),
b"ecdsa-sha2-nistp256" => Ok(Signature::P256(bytes.to_vec())),
b"ecdsa-sha2-nistp256" => Ok(Signature::P256(
p256::ecdsa::Signature::from_slice(bytes)?,
)),
_ => Err(Error::UnknownSignatureType {
sig_type: std::str::from_utf8(typ).unwrap_or("").to_string(),
}),
Expand All @@ -104,7 +100,7 @@ impl AsRef<[u8]> for Signature {
match *self {
Signature::Ed25519(ref signature) => &signature.0,
Signature::RSA { ref bytes, .. } => &bytes[..],
Signature::P256(ref signature) => signature,
Signature::P256(ref signature) => todo!(),
}
}
}
Expand Down
1 change: 0 additions & 1 deletion russh/examples/client_exec_interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use tokio::net::ToSocketAddrs;
#[tokio::main]
async fn main() -> Result<()> {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();

// CLI options are defined later in this file
Expand Down
2 changes: 1 addition & 1 deletion russh/examples/client_exec_simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ use tokio::net::ToSocketAddrs;
#[tokio::main]
async fn main() -> Result<()> {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();

// CLI options are defined later in this file
Expand Down Expand Up @@ -81,6 +80,7 @@ impl Session {
addrs: A,
) -> Result<Self> {
let key_pair = load_secret_key(key_path, None)?;
println!("{:?}", key_pair.public_key_base64());
let config = client::Config {
inactivity_timeout: Some(Duration::from_secs(5)),
..<_>::default()
Expand Down
Loading
Loading