From a277ec65f35046ae6ef7b43afa66540132bf73cf Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 29 Dec 2024 19:03:49 +0100 Subject: [PATCH 01/13] wip --- Cargo.lock | 7 + ssh-key/Cargo.toml | 2 + ssh-key/src/error.rs | 9 + ssh-key/src/kdf.rs | 26 ++ ssh-key/src/lib.rs | 2 + ssh-key/src/ppk.rs | 404 +++++++++++++++++++ ssh-key/src/private.rs | 27 ++ ssh-key/tests/examples/id_dsa_1024.ppk | 17 + ssh-key/tests/examples/id_dsa_1024_enc.ppk | 22 + ssh-key/tests/examples/id_ecdsa_p256.ppk | 10 + ssh-key/tests/examples/id_ecdsa_p256_enc.ppk | 15 + ssh-key/tests/examples/id_ed25519.ppk | 9 + ssh-key/tests/examples/id_ed25519_enc.ppk | 14 + ssh-key/tests/examples/id_rsa_3072.ppk | 36 ++ ssh-key/tests/examples/id_rsa_3072_enc.ppk | 41 ++ ssh-key/tests/private_key.rs | 119 +++++- 16 files changed, 747 insertions(+), 13 deletions(-) create mode 100644 ssh-key/src/ppk.rs create mode 100644 ssh-key/tests/examples/id_dsa_1024.ppk create mode 100644 ssh-key/tests/examples/id_dsa_1024_enc.ppk create mode 100644 ssh-key/tests/examples/id_ecdsa_p256.ppk create mode 100644 ssh-key/tests/examples/id_ecdsa_p256_enc.ppk create mode 100644 ssh-key/tests/examples/id_ed25519.ppk create mode 100644 ssh-key/tests/examples/id_ed25519_enc.ppk create mode 100644 ssh-key/tests/examples/id_rsa_3072.ppk create mode 100644 ssh-key/tests/examples/id_rsa_3072_enc.ppk diff --git a/Cargo.lock b/Cargo.lock index 86571ff8..757cf4ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,6 +365,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hex-literal" version = "0.4.1" @@ -832,6 +838,7 @@ dependencies = [ "bcrypt-pbkdf", "dsa", "ed25519-dalek", + "hex", "hex-literal", "home", "num-bigint-dig", diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index 389e2662..efbd5af5 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -30,6 +30,7 @@ bcrypt-pbkdf = { version = "=0.11.0-pre.1", optional = true, default-features = bigint = { package = "num-bigint-dig", version = "0.8", optional = true, default-features = false } dsa = { version = "=0.7.0-pre.1", optional = true, default-features = false } ed25519-dalek = { version = "=2.2.0-pre", optional = true, default-features = false } +hex = { version = "0.4", optional = true } home = { version = "0.5", optional = true } p256 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] } p384 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] } @@ -79,6 +80,7 @@ getrandom = ["rand_core/getrandom"] p256 = ["dep:p256", "ecdsa"] p384 = ["dep:p384", "ecdsa"] p521 = ["dep:p521", "ecdsa"] +ppk = ["dep:hex", "alloc", "std"] rsa = ["dep:bigint", "dep:rsa", "alloc", "rand_core"] tdes = ["cipher/tdes", "encryption"] diff --git a/ssh-key/src/error.rs b/ssh-key/src/error.rs index 6d1c2792..b208188e 100644 --- a/ssh-key/src/error.rs +++ b/ssh-key/src/error.rs @@ -6,6 +6,9 @@ use core::fmt; #[cfg(feature = "alloc")] use crate::certificate; +#[cfg(feature = "ppk")] +use crate::ppk::PpkParseError; + /// Result type with `ssh-key`'s [`Error`] as the error type. pub type Result = core::result::Result; @@ -66,6 +69,10 @@ pub enum Error { /// Public key is incorrect. PublicKey, + /// PuTTY format parsing errors. + #[cfg(feature = "ppk")] + Ppk(PpkParseError), + /// Invalid timestamp (e.g. in a certificate) Time, @@ -104,6 +111,8 @@ impl fmt::Display for Error { #[cfg(feature = "std")] Error::Io(err) => write!(f, "I/O error: {}", std::io::Error::from(*err)), Error::Namespace => write!(f, "namespace invalid"), + #[cfg(feature = "ppk")] + Error::Ppk(err) => write!(f, "PPK parsing error: {err}"), Error::PublicKey => write!(f, "public key is incorrect"), Error::Time => write!(f, "invalid time"), Error::TrailingData { remaining } => write!( diff --git a/ssh-key/src/kdf.rs b/ssh-key/src/kdf.rs index 9eb47e2d..6e744f62 100644 --- a/ssh-key/src/kdf.rs +++ b/ssh-key/src/kdf.rs @@ -19,6 +19,14 @@ const DEFAULT_BCRYPT_ROUNDS: u32 = 16; #[cfg(feature = "encryption")] const DEFAULT_SALT_SIZE: usize = 16; +#[cfg(feature = "ppk")] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ArgonFlavor { + I, + D, + ID, +} + /// Key Derivation Functions (KDF). #[derive(Clone, Debug, Eq, PartialEq)] #[non_exhaustive] @@ -35,6 +43,16 @@ pub enum Kdf { /// Rounds rounds: u32, }, + + /// Argon2 options. + #[cfg(feature = "ppk")] + Argon2 { + flavor: ArgonFlavor, + memory: u32, + passes: u32, + parallelism: u32, + salt: Vec, + }, } impl Kdf { @@ -62,6 +80,8 @@ impl Kdf { Self::None => KdfAlg::None, #[cfg(feature = "alloc")] Self::Bcrypt { .. } => KdfAlg::Bcrypt, + #[cfg(feature = "ppk")] + Self::Argon2 { .. } => todo!(), } } @@ -74,6 +94,8 @@ impl Kdf { bcrypt_pbkdf(password, salt, *rounds, output).map_err(|_| Error::Crypto)?; Ok(()) } + #[cfg(feature = "ppk")] + Self::Argon2 { .. } => todo!(), } } @@ -163,6 +185,8 @@ impl Encode for Kdf { Self::None => 4, #[cfg(feature = "alloc")] Self::Bcrypt { salt, .. } => [12, salt.len()].checked_sum()?, + #[cfg(feature = "ppk")] + Self::Argon2 { .. } => todo!(), }; [self.algorithm().encoded_len()?, kdfopts_prefixed_len].checked_sum() @@ -179,6 +203,8 @@ impl Encode for Kdf { salt.encode(writer)?; rounds.encode(writer)? } + #[cfg(feature = "ppk")] + Self::Argon2 { .. } => todo!(), } Ok(()) diff --git a/ssh-key/src/lib.rs b/ssh-key/src/lib.rs index 56acda8e..65d055c5 100644 --- a/ssh-key/src/lib.rs +++ b/ssh-key/src/lib.rs @@ -164,6 +164,8 @@ mod mpint; mod signature; #[cfg(feature = "alloc")] mod sshsig; +#[cfg(feature = "ppk")] +mod ppk; pub use crate::{ algorithm::{Algorithm, EcdsaCurve, HashAlg, KdfAlg}, diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs new file mode 100644 index 00000000..3a8465b7 --- /dev/null +++ b/ssh-key/src/ppk.rs @@ -0,0 +1,404 @@ +// Format documentation: +// https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html + +use cipher::Cipher; +use core::fmt::{Debug, Display}; +use core::num::ParseIntError; +use core::str::FromStr; +use hex::FromHex; +use std::collections::HashMap; +use std::string::{String, ToString}; +use std::vec::Vec; + +use encoding::base64::{self, Base64, Encoding}; +use encoding::{Decode, LabelError, Reader}; + +use crate::kdf::ArgonFlavor; +use crate::private::{EcdsaKeypair, KeypairData}; +use crate::public::KeyData; +use crate::{Algorithm, Error, Kdf, Mpint, PublicKey}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum PpkEncryptionAlgorithm { + Aes256Cbc, +} + +impl From for Cipher { + fn from(algorithm: PpkEncryptionAlgorithm) -> Self { + match algorithm { + PpkEncryptionAlgorithm::Aes256Cbc => Cipher::Aes256Cbc, + } + } +} + +impl TryFrom<&str> for ArgonFlavor { + type Error = PpkParseError; + + fn try_from(value: &str) -> Result { + match value { + "Argon2i" => Ok(Self::I), + "Argon2d" => Ok(Self::D), + "Argon2id" => Ok(Self::ID), + _ => Err(PpkParseError::UnsupportedKdf(value.into())), + } + } +} + +pub struct PpkEncryption { + pub algorithm: PpkEncryptionAlgorithm, + pub kdf: Kdf, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PpkKey { + Encryption, + Comment, + Mac, + KeyDerivation, + Argon2Memory, + Argon2Passes, + Argon2Parallelism, + Argon2Salt, +} + +impl TryFrom<&str> for PpkKey { + type Error = (); + + fn try_from(value: &str) -> Result { + match value { + "Encryption" => Ok(PpkKey::Encryption), + "Comment" => Ok(PpkKey::Comment), + "Private-MAC" => Ok(PpkKey::Mac), + "Key-Derivation" => Ok(PpkKey::KeyDerivation), + "Argon2-Memory" => Ok(PpkKey::Argon2Memory), + "Argon2-Passes" => Ok(PpkKey::Argon2Passes), + "Argon2-Parallelism" => Ok(PpkKey::Argon2Parallelism), + "Argon2-Salt" => Ok(PpkKey::Argon2Salt), + _ => Err(()), + } + } +} + +pub struct PpkWrapper { + pub version: u8, + pub algorithm: Algorithm, + pub public_key: Option>, + pub private_key: Option>, + pub values: HashMap, +} + +impl Debug for PpkWrapper { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PpkWrapper") + .field("version", &self.version) + .field("algorithm", &self.algorithm) + .field("public_key", &self.public_key) + .field("values", &self.values) + .finish_non_exhaustive() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PpkParseError { + Algorithm(LabelError), + Header(String), + Syntax(String), + ValueFormat { key: PpkKey, value: String }, + HexFormat(String), // FromHexError does not implement Eq + InvalidInteger(ParseIntError), + UnknownKey(String), + MissingValue(PpkKey), + MissingPublicKey, + MissingPrivateKey, + Base64(base64::InvalidEncodingError), + Eof, + UnsupportedFormatVersion(u8), + UnsupportedEncryption(String), + UnsupportedKdf(String), +} + +impl Display for PpkParseError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Algorithm(err) => write!(f, "invalid algorithm: {:?}", err), + Self::Header(header) => write!(f, "invalid header: {:?}", header), + Self::Syntax(line) => write!(f, "invalid syntax: {:?}", line), + Self::ValueFormat { key, value } => { + write!(f, "invalid value format for key {:?}: {:?}", key, value) + } + Self::HexFormat(err) => write!(f, "invalid hex format: {}", err), + Self::InvalidInteger(err) => write!(f, "invalid integer: {}", err), + Self::UnknownKey(key) => write!(f, "unknown key: {:?}", key), + Self::MissingValue(key) => write!(f, "missing value for key: {:?}", key), + Self::MissingPublicKey => write!(f, "missing public key"), + Self::MissingPrivateKey => write!(f, "missing private key"), + Self::Base64(err) => write!(f, "base64 decode: {}", err), + Self::Eof => write!(f, "unexpected end of file"), + Self::UnsupportedFormatVersion(version) => { + write!(f, "unsupported format version: {}", version) + } + Self::UnsupportedEncryption(encryption) => { + write!(f, "unsupported encryption mode: {:?}", encryption) + } + Self::UnsupportedKdf(kdf) => write!(f, "unsupported KDF: {:?}", kdf), + } + } +} + +impl From for Error { + fn from(err: PpkParseError) -> Self { + Error::Ppk(err) + } +} + +const PPK_HEADER_PREFIX: &str = "PuTTY-User-Key-File-"; + +impl TryFrom<&str> for PpkWrapper { + type Error = PpkParseError; + + fn try_from(contents: &str) -> Result { + let mut lines = contents.lines(); + let header = lines.next().ok_or(PpkParseError::Eof)?; + let Some(header) = header.strip_prefix(PPK_HEADER_PREFIX) else { + return Err(PpkParseError::Header(header.into())); + }; + + let (header_version, header_algorithm) = header + .split_once(": ") + .ok_or(PpkParseError::Header(header.into()))?; + + let version = header_version + .parse() + .map_err(|_| PpkParseError::Header(header.into()))?; + if version != 3 { + return Err(PpkParseError::UnsupportedFormatVersion(version)); + } + + let algorithm = Algorithm::from_str(header_algorithm).map_err(PpkParseError::Algorithm)?; + let mut public_key = None; + let mut private_key = None; + + let mut values = HashMap::new(); + while let Some(line) = lines.next() { + let (key, value) = line + .split_once(": ") + .ok_or(PpkParseError::Syntax(line.into()))?; + + if key.ends_with("-Lines") { + let n_lines: usize = value.parse().map_err(PpkParseError::InvalidInteger)?; + + let mut content = Vec::new(); + for _ in 0..n_lines { + let line = lines.next().ok_or(PpkParseError::Eof)?; + content.extend_from_slice(line.as_bytes()); + } + + let decoded = + Base64::decode_in_place(&mut content).map_err(PpkParseError::Base64)?; + + match key { + "Public-Lines" => public_key = Some(decoded.to_vec()), + "Private-Lines" => private_key = Some(decoded.to_vec()), + _ => return Err(PpkParseError::UnknownKey(key.into())), + } + } else { + let key = + PpkKey::try_from(key).map_err(|_| PpkParseError::UnknownKey(key.into()))?; + + values.insert(key, value.to_string()); + } + } + + Ok(PpkWrapper { + version, + algorithm, + public_key, + private_key, + values, + }) + } +} + +pub struct PpkContainer { + pub version: u8, + pub encryption: Option, + pub mac: Vec, + pub public_key: PublicKey, + pub keypair_data: KeypairData, +} + +impl TryFrom for PpkContainer { + type Error = Error; + + fn try_from(mut ppk: PpkWrapper) -> Result { + let encryption = match ppk.values.get(&PpkKey::Encryption).map(String::as_str) { + None | Some("none") => None, + Some("aes256-cbc") => { + let parse_int = |key: PpkKey| -> Result { + ppk.values + .get(&key) + .ok_or(PpkParseError::MissingValue(key)) + .and_then(|v| v.parse().map_err(PpkParseError::InvalidInteger)) + }; + + match ppk.values.get(&PpkKey::KeyDerivation).map(String::as_str) { + None => { + return Err(PpkParseError::MissingValue(PpkKey::KeyDerivation).into()); + } + Some(kdf) => Some(PpkEncryption { + algorithm: PpkEncryptionAlgorithm::Aes256Cbc, + kdf: Kdf::Argon2 { + flavor: ArgonFlavor::try_from(kdf)?, + memory: parse_int(PpkKey::Argon2Memory)?, + passes: parse_int(PpkKey::Argon2Passes)?, + parallelism: parse_int(PpkKey::Argon2Parallelism)?, + salt: Vec::from_hex( + ppk.values + .get(&PpkKey::Argon2Salt) + .ok_or(PpkParseError::MissingValue(PpkKey::Argon2Salt))?, + ) + .map_err(|e| PpkParseError::HexFormat(e.to_string()))?, + }, + }), + } + } + Some(v) => return Err(PpkParseError::UnsupportedEncryption(v.into()).into()), + }; + + let mac = Vec::from_hex( + ppk.values + .get(&PpkKey::Mac) + .ok_or(PpkParseError::MissingValue(PpkKey::Mac))?, + ) + .map_err(|e| PpkParseError::HexFormat(e.to_string()))?; + + let public_key = ppk.public_key.ok_or(PpkParseError::MissingPublicKey)?; + let private_key = ppk.private_key.ok_or(PpkParseError::MissingPrivateKey)?; + + let mut public_key = PublicKey::from_bytes(&public_key)?; + let mut private_key_cursor = &private_key[..]; + let keypair_data = match encryption { + Some(_) => todo!(), + None => { + decode_private_key_as(&mut private_key_cursor, public_key.clone(), ppk.algorithm)? + } + }; + + let comment = ppk.values.remove(&PpkKey::Comment); + public_key.comment = comment.unwrap_or_default(); + + // todo verify mac + + Ok(PpkContainer { + version: ppk.version, + encryption, + mac, + public_key, + keypair_data, + }) + } +} + +fn decode_private_key_as( + reader: &mut impl Reader, + public: PublicKey, + algorithm: Algorithm, +) -> Result { + match (&algorithm, public.key_data()) { + (Algorithm::Dsa { .. }, KeyData::Dsa(pk)) => { + use crate::private::{DsaKeypair, DsaPrivateKey}; + Ok(KeypairData::Dsa(DsaKeypair { + private: DsaPrivateKey::decode(reader)?, + public: pk.clone(), + })) + } + + #[cfg(feature = "rsa")] + (Algorithm::Rsa { .. }, KeyData::Rsa(pk)) => { + use crate::private::{RsaKeypair, RsaPrivateKey}; + + let d = Mpint::decode(reader)?; + let p = Mpint::decode(reader)?; + let q = Mpint::decode(reader)?; + let iqmp = Mpint::decode(reader)?; + let private = RsaPrivateKey { d, iqmp, p, q }; + Ok(KeypairData::Rsa(RsaKeypair { + private, + public: pk.clone(), + })) + } + + #[cfg(feature = "ed25519")] + (Algorithm::Ed25519 { .. }, KeyData::Ed25519(pk)) => { + // PPK encodes Ed25519 private exponent as an mpint + use crate::private::{Ed25519Keypair, Ed25519PrivateKey}; + use zeroize::Zeroizing; + + // Copy and pad exponent + let mut buf = Zeroizing::new([0u8; Ed25519PrivateKey::BYTE_SIZE]); + let e = Mpint::decode(reader)?; + let e_bytes = e.as_bytes(); + assert!(e_bytes.len() <= buf.len()); + buf[Ed25519PrivateKey::BYTE_SIZE - e_bytes.len()..].copy_from_slice(e_bytes); + + let private = Ed25519PrivateKey::from_bytes(&buf); + Ok(KeypairData::Ed25519(Ed25519Keypair { + public: pk.clone(), + private, + })) + } + + #[cfg(feature = "ecdsa")] + (Algorithm::Ecdsa { curve }, KeyData::Ecdsa(public)) => { + // PPK encodes EcDSA private exponent as an mpint + use crate::public::EcdsaPublicKey; + use crate::EcdsaCurve; + + // Copy and pad exponent + let e = Mpint::decode(reader)?; + let e_bytes = e.as_positive_bytes().ok_or(Error::Crypto)?; + if e_bytes.len() > curve.field_size() { + return Err(Error::Crypto); + } + + type EC = EcdsaCurve; + type EPK = EcdsaPublicKey; + type EKP = EcdsaKeypair; + + let keypair: EKP = match (curve, public) { + #[cfg(feature = "p256")] + (EC::NistP256, EPK::NistP256(public)) => EKP::NistP256 { + public: public.clone(), + private: p256::SecretKey::from_slice(e_bytes) + .map_err(|_| Error::Crypto)? + .into(), + }, + #[cfg(feature = "p384")] + (EC::NistP384, EPK::NistP384(public)) => EKP::NistP384 { + public: public.clone(), + private: p384::SecretKey::from_slice(e_bytes) + .map_err(|_| Error::Crypto)? + .into(), + }, + #[cfg(feature = "p521")] + (EC::NistP521, EPK::NistP521(public)) => EKP::NistP521 { + public: public.clone(), + private: p521::SecretKey::from_slice(e_bytes) + .map_err(|_| Error::Crypto)? + .into(), + }, + _ => return Err(Error::Crypto), + }; + Ok(keypair.into()) + } + _ => Err(algorithm.unsupported_error()), + } +} + +impl TryFrom<&str> for PpkContainer { + type Error = Error; + + fn try_from(contents: &str) -> Result { + PpkWrapper::try_from(contents.as_ref())?.try_into() + } +} diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index 311a1fe2..dbed9174 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -233,6 +233,33 @@ impl PrivateKey { Self::decode_pem(pem) } + /// Parse a PuTTY PPK private key. + /// + /// PPK-formatted private keys begin with the following: + /// + /// ```text + /// PuTTY-User-Key-File-: + /// ``` + #[cfg(feature = "ppk")] + pub fn from_ppk(ppk: impl AsRef) -> Result { + use crate::ppk::PpkContainer; + + let ppk: PpkContainer = PpkContainer::try_from(ppk.as_ref())?; + + Ok(Self { + auth_tag: None, + checkint: None, + cipher: ppk + .encryption + .as_ref() + .map(|e| e.algorithm.into()) + .unwrap_or(Cipher::None), + kdf: ppk.encryption.map(|x| x.kdf).unwrap_or(Kdf::None), + key_data: ppk.keypair_data, + public_key: ppk.public_key, + }) + } + /// Parse a raw binary SSH private key. pub fn from_bytes(mut bytes: &[u8]) -> Result { let reader = &mut bytes; diff --git a/ssh-key/tests/examples/id_dsa_1024.ppk b/ssh-key/tests/examples/id_dsa_1024.ppk new file mode 100644 index 00000000..879a62e5 --- /dev/null +++ b/ssh-key/tests/examples/id_dsa_1024.ppk @@ -0,0 +1,17 @@ +PuTTY-User-Key-File-3: ssh-dss +Encryption: none +Comment: user@example.com +Public-Lines: 10 +AAAAB3NzaC1kc3MAAACBANw9iSUO2UYhFMssjUgW46URqv8bBrDgHeF8HLBOWBvK +uXF2Rx2J/XyhgX48SOLMuv0hcPaejlyLarabnF9F2V4dkpPpZSJ+7luHmxEjNxwh +sdtg8UteXAWkeCzrQ6MvRJZHcDBjYh56KGvslbFnJsGLXlI4PQCyl6awNImwYGil +AAAAFQCJGBU3hZf+QtP9Jh/nbfNlhFu7hwAAAIBHObOQioQVRm3HsVb7mOy3FVKh +cLoLO3qoG9gTkd4KeuehtFAC3+rckiX7xSCnE/5BBKdL7VP9WRXac2Nlr9Pwl3e7 +zPut96wrCHt/TZX6vkfXKkbpUIj5zSqfvyNrWKaYJkfzwAQwrXNS1Hol676Ud/DD +En2oatdEhkS3beWHXAAAAIBgQqaz/YYTRMshzMzYcZ4lqgvgmA55y6v0h39e8HH2 +A5dwNS6sPUw2jyna+le0dceNRJifFld1J+WYM0vmquSr11DDavgEidOSaXwfMvPP +PJqLmbzdtT16N+Gij9U9STQTHPQcQ3xnNNHgQAStzZJbhLOVbDDDo5BO7LMUALDf +SA== +Private-Lines: 1 +AAAAFAw3esRJ53DYmjVXdDy9BQOWEUti +Private-MAC: c81aa8c9184f65cadf48deefc7866ed98b66dc887dd1553068aac804c7f35705 diff --git a/ssh-key/tests/examples/id_dsa_1024_enc.ppk b/ssh-key/tests/examples/id_dsa_1024_enc.ppk new file mode 100644 index 00000000..f3228b9d --- /dev/null +++ b/ssh-key/tests/examples/id_dsa_1024_enc.ppk @@ -0,0 +1,22 @@ +PuTTY-User-Key-File-3: ssh-dss +Encryption: aes256-cbc +Comment: user@example.com +Public-Lines: 10 +AAAAB3NzaC1kc3MAAACBANw9iSUO2UYhFMssjUgW46URqv8bBrDgHeF8HLBOWBvK +uXF2Rx2J/XyhgX48SOLMuv0hcPaejlyLarabnF9F2V4dkpPpZSJ+7luHmxEjNxwh +sdtg8UteXAWkeCzrQ6MvRJZHcDBjYh56KGvslbFnJsGLXlI4PQCyl6awNImwYGil +AAAAFQCJGBU3hZf+QtP9Jh/nbfNlhFu7hwAAAIBHObOQioQVRm3HsVb7mOy3FVKh +cLoLO3qoG9gTkd4KeuehtFAC3+rckiX7xSCnE/5BBKdL7VP9WRXac2Nlr9Pwl3e7 +zPut96wrCHt/TZX6vkfXKkbpUIj5zSqfvyNrWKaYJkfzwAQwrXNS1Hol676Ud/DD +En2oatdEhkS3beWHXAAAAIBgQqaz/YYTRMshzMzYcZ4lqgvgmA55y6v0h39e8HH2 +A5dwNS6sPUw2jyna+le0dceNRJifFld1J+WYM0vmquSr11DDavgEidOSaXwfMvPP +PJqLmbzdtT16N+Gij9U9STQTHPQcQ3xnNNHgQAStzZJbhLOVbDDDo5BO7LMUALDf +SA== +Key-Derivation: Argon2id +Argon2-Memory: 8192 +Argon2-Passes: 21 +Argon2-Parallelism: 1 +Argon2-Salt: bafba596db8b0897f098f623622ed05e +Private-Lines: 1 +EijTeedP2qRkTbtbgvi+urPEQB55YjpOXmpIClewDYg= +Private-MAC: fe48b75517f8d060232a6771846fdab826b447b8b0982e56dc210554824acdbf diff --git a/ssh-key/tests/examples/id_ecdsa_p256.ppk b/ssh-key/tests/examples/id_ecdsa_p256.ppk new file mode 100644 index 00000000..a3936805 --- /dev/null +++ b/ssh-key/tests/examples/id_ecdsa_p256.ppk @@ -0,0 +1,10 @@ +PuTTY-User-Key-File-3: ecdsa-sha2-nistp256 +Encryption: none +Comment: user@example.com +Public-Lines: 3 +AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHwf2HMM5TRX +vo2SQJjsNkiDD5KqiiNjrGVv3UUh+mMT5RHxiRtOnlqvjhQtBq0VpmpCV/PwUdhO +ig4vkbqAcEc= +Private-Lines: 1 +AAAAIQDKeKZHdL+uNxIyJJN/A5iWAYlwesoKhkXOtDWcQjugeQ== +Private-MAC: 4df1d2227cb63bbbaf0821816cb82afffc2c97163d22afb114f68621a55b07c7 diff --git a/ssh-key/tests/examples/id_ecdsa_p256_enc.ppk b/ssh-key/tests/examples/id_ecdsa_p256_enc.ppk new file mode 100644 index 00000000..11dbb105 --- /dev/null +++ b/ssh-key/tests/examples/id_ecdsa_p256_enc.ppk @@ -0,0 +1,15 @@ +PuTTY-User-Key-File-3: ecdsa-sha2-nistp256 +Encryption: aes256-cbc +Comment: user@example.com +Public-Lines: 3 +AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHwf2HMM5TRX +vo2SQJjsNkiDD5KqiiNjrGVv3UUh+mMT5RHxiRtOnlqvjhQtBq0VpmpCV/PwUdhO +ig4vkbqAcEc= +Key-Derivation: Argon2id +Argon2-Memory: 8192 +Argon2-Passes: 34 +Argon2-Parallelism: 1 +Argon2-Salt: eb972eedc0ef9ddff73e9e960b049d53 +Private-Lines: 1 +3PWKqAg767DkT7bs1mq6TiP5Pfts+iIbfmIzMJaIMYmUObdDqtY6sosPRNn1b+Jq +Private-MAC: 40a65df11818f2967620fa620571fb153a3df74972048af64f253112a0f87722 diff --git a/ssh-key/tests/examples/id_ed25519.ppk b/ssh-key/tests/examples/id_ed25519.ppk new file mode 100644 index 00000000..33fdaf17 --- /dev/null +++ b/ssh-key/tests/examples/id_ed25519.ppk @@ -0,0 +1,9 @@ +PuTTY-User-Key-File-3: ssh-ed25519 +Encryption: none +Comment: user@example.com +Public-Lines: 2 +AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XF +Sqti +Private-Lines: 1 +AAAAILYGwiLRDBba4WxwpNRRc0cuxhfgXGVpINJuVsCPtZHt +Private-MAC: 94140d0344fad6aa1bf7b71e9c93db11ccac8a232f8a51e11c024869d608c82d diff --git a/ssh-key/tests/examples/id_ed25519_enc.ppk b/ssh-key/tests/examples/id_ed25519_enc.ppk new file mode 100644 index 00000000..f3a9f569 --- /dev/null +++ b/ssh-key/tests/examples/id_ed25519_enc.ppk @@ -0,0 +1,14 @@ +PuTTY-User-Key-File-3: ssh-ed25519 +Encryption: aes256-cbc +Comment: user@example.com +Public-Lines: 2 +AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XF +Sqti +Key-Derivation: Argon2id +Argon2-Memory: 8192 +Argon2-Passes: 34 +Argon2-Parallelism: 1 +Argon2-Salt: 63d1d43f7bf7700720496646a2f5ec17 +Private-Lines: 1 +DyWtExZ3dxFutnb12tIwXBC6kWdozrvP+r6faHKBGDb4+qEar9XBiC0BmGySMHUi +Private-MAC: 52fd00d4ef47ebc506e4e709486c0c6bc0606e24fe2c6cb1b3d168f4da238a66 diff --git a/ssh-key/tests/examples/id_rsa_3072.ppk b/ssh-key/tests/examples/id_rsa_3072.ppk new file mode 100644 index 00000000..51e2db72 --- /dev/null +++ b/ssh-key/tests/examples/id_rsa_3072.ppk @@ -0,0 +1,36 @@ +PuTTY-User-Key-File-3: ssh-rsa +Encryption: none +Comment: user@example.com +Public-Lines: 9 +AAAAB3NzaC1yc2EAAAADAQABAAABgQCmjkeMm8k3JkNrf16eb5pG4bc77B6Mt3VN +4saltsRV8vASpyWa/PlBgdaeldOaNJ5NK0gqU3KyiUNzHbdcc8572e7IUBDJS/rl +aWARiSL4aos2VbNX0k56Z5zYp9m/bq5m9/mlb+PQkNBjIhimgpYNiq2TwBiYeA6t +Lb79cPtHA0cX5BLk/a5oUpLsiR4kI/f+Q98vVDKasKXXVh5YLkLobrruDB6er2A9 +fOcIUF0O4JCRLh/Dc161gE3fQrYTMQenbppZzfxrZfQ8YwLPvKjnqm+XRX+pbTta +Juj0EgTSzUK+EZxoSw8CNwiZpxrjwecTMVQ8w/srQmh4ABGuTqk0wP8HcI7hg+fp +Bv7kiejh5X/Oehxt+Puu85u9GVXb1a0av/vhJvUCBcuISvCA/z1wVJ0xdLhb1/Zi +TDdTzyNbZQ0OQijzK+e1SlkNhp+3eGVZu3pNZvnTppwIXv3wg6kV1HodkWGgh1ay +Y7Buc52Z8okDYqvJat5CzOj5OaQNr/k= +Private-Lines: 21 +AAABgGtjCxDGlQrA2fFicxA2JsOS3sB88gmKc9Ce6bOIzrgX5eAw8tcmSlOJMmaX +dZJUYMiiomnf2fDw/ZMoUsQCStyh3Ao9TUVsfr0RnwZPZEPE9jM3OGXkTAMx8Pfj +6Uo7Q6lSMx0OslUUObfhEQGy6qqagmXkEjekGNphx2XDRdA4dcsam3AXfC75Jo/p +rIxiwI+pFSp/4AzK3nKjrPbwBOW2F0JKgCeSLbwXXyKGJinkcnGYypQLO8JMkmjj +q19eWWW4OH4UcGebPqaAll+BWTyxQTENTEFWniWzdqLcTtkvkUm3XpcOgiRzCUbM +IPNR+BFbG7/Ls49r0GxiBHK3bWQdNYAq3vFSIKubKlfjWRj+J+E4EZzKVqmMzzwP +xoOhnychqHZuzdnFdndmJlbz0+BTJfP7NzJmI9u+xjs9mEgwst0nvrtr0u1TRd// +GN8YBq3rztqYRYBJaJMGgaw+UjE4xSFssTWZfj4UOngWrMPYdB6s7H4V9T2g8IEG +kXCNnQAAAMEA0R564khkDTsgKTaRiGVEzf4HeamqtWyPlia/HmZIv9mIvbCsfRGn +PjQFYzbUrTkA/3GE7kBLhLrrEaKjAvmC2U7vt1cDDsbXfZEV6u+Aq1dJoPW1kLKZ +/96U+ZMN7bqyrzMwlbCKUEubMPERLc5R837QDQQzQ9Qg0uL7iL1/iBt8iZDki5P9 +HShPzIwcB/vvwE0CklsvFZqan1Zwc+HJT9xuRy9IljvhbFxUU4Vq0r95FuQsNuda +UBiRDY2tA41zAAAAwQDL5Q5+zfXiyG52ypS+iwwFsJBB0rzd7rRnLnEg6syDgOXW +t3yFWDxQj47o1VfKvLbfroxyOF8PaTRevBWl3+yUnAdw0C15Rd01klYtpziGYuBT +xUVNJpDeKmPMVV4aAQ4toK4wfRwR+FKpx1aOAvk9SbKo+Se3mUOykgytMhqiCEEJ +0TbQhcHQXDn0w2z4n9w8ZqdV5j9EbhYwKxNZlADwqDMhoua5FT3wLwPeMY6gkDko +KFPyAR4JBdEVdmfK8eMAAADAVEBapmOunggANacQAvTDUdfQAsNSAHJebcD/bZAa +MEsQOi6gFlB5ltMZNYtb6k/rQJj1MFKPErmMUMfd/IX8Svkle6+apyNc30Z3NJt3 +5SpApeL0QSLRjOQJQZFOmRacSLcIiY0phpZWYHt+LrY1QeC71Wjk93S+wxN9AqWR +yMd7LhiN1vcu71z/GSfN5XOkyg1DwrbGqVchRFEi4c9qpfBbZcuchhJPn3n6KfBe +PwbzuD7cqZQfVxZQ4PtGiq5M +Private-MAC: 0af82982a74c03dfc50f53e31eddebbfedb306147d0579c069d86f3f384f8940 diff --git a/ssh-key/tests/examples/id_rsa_3072_enc.ppk b/ssh-key/tests/examples/id_rsa_3072_enc.ppk new file mode 100644 index 00000000..081f85e6 --- /dev/null +++ b/ssh-key/tests/examples/id_rsa_3072_enc.ppk @@ -0,0 +1,41 @@ +PuTTY-User-Key-File-3: ssh-rsa +Encryption: aes256-cbc +Comment: user@example.com +Public-Lines: 9 +AAAAB3NzaC1yc2EAAAADAQABAAABgQCmjkeMm8k3JkNrf16eb5pG4bc77B6Mt3VN +4saltsRV8vASpyWa/PlBgdaeldOaNJ5NK0gqU3KyiUNzHbdcc8572e7IUBDJS/rl +aWARiSL4aos2VbNX0k56Z5zYp9m/bq5m9/mlb+PQkNBjIhimgpYNiq2TwBiYeA6t +Lb79cPtHA0cX5BLk/a5oUpLsiR4kI/f+Q98vVDKasKXXVh5YLkLobrruDB6er2A9 +fOcIUF0O4JCRLh/Dc161gE3fQrYTMQenbppZzfxrZfQ8YwLPvKjnqm+XRX+pbTta +Juj0EgTSzUK+EZxoSw8CNwiZpxrjwecTMVQ8w/srQmh4ABGuTqk0wP8HcI7hg+fp +Bv7kiejh5X/Oehxt+Puu85u9GVXb1a0av/vhJvUCBcuISvCA/z1wVJ0xdLhb1/Zi +TDdTzyNbZQ0OQijzK+e1SlkNhp+3eGVZu3pNZvnTppwIXv3wg6kV1HodkWGgh1ay +Y7Buc52Z8okDYqvJat5CzOj5OaQNr/k= +Key-Derivation: Argon2id +Argon2-Memory: 8192 +Argon2-Passes: 34 +Argon2-Parallelism: 1 +Argon2-Salt: d83ba92c892399aebda99cd8f905b8b8 +Private-Lines: 21 +TlFHWqvS1sF6IEFugWgOCTo3uPhxBmmbOaEwTBvghPGWur7ZC8QtLWlh+waoIRCq +NBvHpzKYueiKdUdF+JQAsHOoDAaZtWVW96+x/FpCjXkRv4JmVGhcpwQ36IaXWqJ7 +eR2ehkGb/T/gaX0kHF6rlGJzUtyMA7N8vnobEByax+8jwHupMkbbOWvP8DzxrfBs +YnHrf3hOcyUzuGXMOFPwgdR+kjqBLQdFugJ0abe7o5fEcbZM7ZxnFYx1AcrWvNlC +J1LDu52WDtfU51rgdiRjF1c7cxNN6eLFvHkbWLPAOyQhgdFJatzssQbweZnpx4Rr +MzoQ6swohqBL2SKZJnUxwJb9zo39dA+q7vpT1H8KxNnmvkdrc4Af+mhF6HrPjS0S +ek8sa6wPLQkUWAu6ikV21udrG8XN8yeT0nVy58DS0EHX7VrQwJL/cvczuo1ZdKhA +YBCQUM8Iao0oRw9LGMjoa40IW+eHLBdIgz+Te3fWb2QQhX+RbjNwXx2c0udRLdzX +qiVSDJj321EJIN19e4a20kkk8hyIPItlwUsKHh/IM1oLaM6p6XGqA1EpxL4QuMqs +OXRA7aVJwMSFf5E+JJ8MFtCf2N4/tbsznrLhQt4t+qmKx0FH/cyZ3th6Pz24F3n9 +oDbHugfagMrRFHUOhJPb+wj5dbd1XJqr8T+1jC1cT27qANpqrGgWy5JkHw1WpSEL +W7nLXH5nSVuIvdhrG09R9B3+Gia8xqo6RqZTST+AkfEmUOwvJSjrCFS1I5IrtinC ++fcRD5URLJx1JjmWsFZl7HcxlqKCe60tGF9lK8Vu+UT4hWGwKFN6RjkP/pyVlaVi +XZuaRM7p28ld2u1LjgrfEMkOHBe+lJ3GQLCuuiNyVD8yzGxXLNTp75TzLglYI6Bn +rSoFwp7D0BkzC5CYUzFTAxSH5UrUoIQYXB54rY/PUVWpvuU7z/Wnxcx5DhiScL44 +jtk+K6YDcoT1soszxmwfxmJR6MxBqPUnaMwD8I/paT6ju6Bmm17X4mc9rK++83Dz +LCIviCr8Cv7PFInBtcazLCbHl4X8nT+7j7Ju6NVMaL7B92PNTrOwEv9TT4y8sIVw +O1zU1aR1kXBXg87y3BXrfcixReu23mUqyjRjKwryqiDbPEKy0S82Zfig6Jfb1FBf +haAqiYuiq7DQgJ2ADf+UW+finVZovAiVjgfzgbxSbwTE2GOCl4IQK7jOykRcrUXf +KRdPkC/+Zb9m/h3CqyWJfotN41c/fj6YJqIF0zQciDvcO9MN+Gon1SJkhdyKSQUh +Pan228lwua8sb7FFL0boWizRFxVMu62CtlhXkMG5CJM= +Private-MAC: 1922d95963690ea168d22290b7fcf19d021a0c105bf54258c0fb59d99f99ae66 diff --git a/ssh-key/tests/private_key.rs b/ssh-key/tests/private_key.rs index a6b3eb25..ed1ce9a4 100644 --- a/ssh-key/tests/private_key.rs +++ b/ssh-key/tests/private_key.rs @@ -19,21 +19,45 @@ use { #[cfg(feature = "alloc")] const OPENSSH_DSA_EXAMPLE: &str = include_str!("examples/id_dsa_1024"); +/// Same key, converted by puttygen +#[cfg(all(feature = "ppk", feature = "alloc"))] +const PPK_DSA_EXAMPLE: &str = include_str!("examples/id_dsa_1024.ppk"); + +/// Same key, converted and encrypted by puttygen +#[cfg(all(feature = "ppk", feature = "alloc", feature = "encryption"))] +const PPK_DSA_EXAMPLE_ENCRYPTED: &str = include_str!("examples/id_dsa_1024_enc.ppk"); + /// ECDSA/P-256 OpenSSH-formatted public key -#[cfg(feature = "ecdsa")] +#[cfg(feature = "p256")] const OPENSSH_ECDSA_P256_EXAMPLE: &str = include_str!("examples/id_ecdsa_p256"); +/// Same key, converted by puttygen +#[cfg(all(feature = "ppk", feature = "p256"))] +const PPK_ECDSA_P256_EXAMPLE: &str = include_str!("examples/id_ecdsa_p256.ppk"); + +/// Same key, converted and encrypted by puttygen +#[cfg(all(feature = "ppk", feature = "p256", feature = "encryption"))] +const PPK_ECDSA_P256_EXAMPLE_ENCRYPTED: &str = include_str!("examples/id_ecdsa_p256_enc.ppk"); + /// ECDSA/P-384 OpenSSH-formatted public key -#[cfg(feature = "ecdsa")] +#[cfg(feature = "p384")] const OPENSSH_ECDSA_P384_EXAMPLE: &str = include_str!("examples/id_ecdsa_p384"); /// ECDSA/P-521 OpenSSH-formatted public key -#[cfg(feature = "ecdsa")] +#[cfg(feature = "p521")] const OPENSSH_ECDSA_P521_EXAMPLE: &str = include_str!("examples/id_ecdsa_p521"); /// Ed25519 OpenSSH-formatted private key const OPENSSH_ED25519_EXAMPLE: &str = include_str!("examples/id_ed25519"); +/// Same key, converted by puttygen +#[cfg(all(feature = "ppk", feature = "ed25519"))] +const PPK_ED25519_EXAMPLE: &str = include_str!("examples/id_ed25519.ppk"); + +/// Same key, converted and encrypted by puttygen +#[cfg(all(feature = "ppk", feature = "ed25519", feature = "encryption"))] +const PPK_ED25519_EXAMPLE_ENCRYPTED: &str = include_str!("examples/id_ed25519_enc.ppk"); + /// Ed25519 OpenSSH-formatted private key with 64-column line wrapping const OPENSSH_ED25519_64COLS_EXAMPLE: &str = include_str!("examples/id_ed25519.64cols"); @@ -41,6 +65,14 @@ const OPENSSH_ED25519_64COLS_EXAMPLE: &str = include_str!("examples/id_ed25519.6 #[cfg(feature = "alloc")] const OPENSSH_RSA_3072_EXAMPLE: &str = include_str!("examples/id_rsa_3072"); +/// Same key, converted by puttygen +#[cfg(feature = "ppk")] +const PPK_RSA_3072_EXAMPLE: &str = include_str!("examples/id_rsa_3072.ppk"); + +/// Same key, converted and encrypted by puttygen +#[cfg(all(feature = "ppk", feature = "encryption"))] +const PPK_RSA_3072_EXAMPLE_ENCRYPTED: &str = include_str!("examples/id_rsa_3072_enc.ppk"); + /// RSA (4096-bit) OpenSSH-formatted public key #[cfg(feature = "alloc")] const OPENSSH_RSA_4096_EXAMPLE: &str = include_str!("examples/id_rsa_4096"); @@ -63,7 +95,23 @@ pub fn scratch_path(filename: &str) -> PathBuf { #[cfg(feature = "alloc")] #[test] fn decode_dsa_openssh() { - let key = PrivateKey::from_openssh(OPENSSH_DSA_EXAMPLE).unwrap(); + validate_dsa(PrivateKey::from_openssh(OPENSSH_DSA_EXAMPLE).unwrap()); +} + +#[cfg(all(feature = "ppk", feature = "alloc"))] +#[test] +fn decode_dsa_ppk() { + validate_dsa(PrivateKey::from_ppk(PPK_DSA_EXAMPLE).unwrap()); +} + +// #[cfg(all(feature = "ppk", feature = "alloc", feature = "encryption"))] +// #[test] +// fn decode_dsa_ppk_encrypted() { +// validate_dsa(PrivateKey::from_ppk(PPK_DSA_EXAMPLE_ENCRYPTED).unwrap()); +// } + +#[cfg(feature = "alloc")] +fn validate_dsa(key: PrivateKey) { assert_eq!(Algorithm::Dsa, key.algorithm()); assert_eq!(Cipher::None, key.cipher()); assert_eq!(KdfAlg::None, key.kdf().algorithm()); @@ -105,10 +153,25 @@ fn decode_dsa_openssh() { assert_eq!("user@example.com", key.comment()); } -#[cfg(feature = "ecdsa")] +#[cfg(feature = "p256")] #[test] fn decode_ecdsa_p256_openssh() { - let key = PrivateKey::from_openssh(OPENSSH_ECDSA_P256_EXAMPLE).unwrap(); + validate_ecdsa_p256(PrivateKey::from_openssh(OPENSSH_ECDSA_P256_EXAMPLE).unwrap()); +} + +#[cfg(all(feature = "ppk", feature = "p256"))] +#[test] +fn decode_ecdsa_p256_ppk() { + validate_ecdsa_p256(PrivateKey::from_ppk(PPK_ECDSA_P256_EXAMPLE).unwrap()); +} + +// #[cfg(all(feature = "ppk", feature = "p256", feature = "encryption"))] +// #[test] +// fn decode_ecdsa_p256_ppk_encrypted() { +// validate_ecdsa_p256(PrivateKey::from_ppk(PPK_ECDSA_P256_EXAMPLE_ENCRYPTED).unwrap()); +// } + +fn validate_ecdsa_p256(key: PrivateKey) { assert_eq!( Algorithm::Ecdsa { curve: EcdsaCurve::NistP256 @@ -137,7 +200,7 @@ fn decode_ecdsa_p256_openssh() { assert_eq!("user@example.com", key.comment()); } -#[cfg(feature = "ecdsa")] +#[cfg(feature = "p384")] #[test] fn decode_padless_wonder_openssh() { let key = PrivateKey::from_openssh(OPENSSH_PADLESS_WONDER_EXAMPLE).unwrap(); @@ -191,7 +254,7 @@ fn decode_ecdsa_p384_openssh() { assert_eq!("user@example.com", key.comment()); } -#[cfg(feature = "ecdsa")] +#[cfg(feature = "p521")] #[test] fn decode_ecdsa_p521_openssh() { let key = PrivateKey::from_openssh(OPENSSH_ECDSA_P521_EXAMPLE).unwrap(); @@ -230,7 +293,22 @@ fn decode_ecdsa_p521_openssh() { #[test] fn decode_ed25519_openssh() { - let key = PrivateKey::from_openssh(OPENSSH_ED25519_EXAMPLE).unwrap(); + validate_ed25519(PrivateKey::from_openssh(OPENSSH_ED25519_EXAMPLE).unwrap()); +} + +#[test] +#[cfg(all(feature = "ppk", feature = "ed25519"))] +fn decode_ed25519_ppk() { + validate_ed25519(PrivateKey::from_ppk(PPK_ED25519_EXAMPLE).unwrap()); +} + +// #[test] +// #[cfg(all(feature = "ppk", feature="ed25519",feature = "encryption"))] +// fn decode_ed25519_ppk_encrypted() { +// validate_ed25519(PrivateKey::from_ppk(PPK_ED25519_EXAMPLE_ENCRYPTED).unwrap()); +// } + +fn validate_ed25519(key: PrivateKey) { assert_eq!(Algorithm::Ed25519, key.algorithm()); assert_eq!(Cipher::None, key.cipher()); assert_eq!(KdfAlg::None, key.kdf().algorithm()); @@ -261,7 +339,22 @@ fn decode_ed25519_openssh_64cols() { #[cfg(feature = "alloc")] #[test] fn decode_rsa_3072_openssh() { - let key = PrivateKey::from_openssh(OPENSSH_RSA_3072_EXAMPLE).unwrap(); + validate_rsa_3072(PrivateKey::from_openssh(OPENSSH_RSA_3072_EXAMPLE).unwrap()); +} + +#[test] +#[cfg(feature = "ppk")] +fn decode_rsa_3072_ppk() { + validate_rsa_3072(PrivateKey::from_ppk(PPK_RSA_3072_EXAMPLE).unwrap()); +} + +// #[test] +// #[cfg(feature = "ppk")] +// fn decode_rsa_3072_ppk_encrypted() { +// validate_rsa_3072(PrivateKey::from_ppk(PPK_RSA_3072_EXAMPLE_ENCRYPTED).unwrap()); +// } + +fn validate_rsa_3072(key: PrivateKey) { assert_eq!(Algorithm::Rsa { hash: None }, key.algorithm()); assert_eq!(Cipher::None, key.cipher()); assert_eq!(KdfAlg::None, key.kdf().algorithm()); @@ -457,19 +550,19 @@ fn encode_dsa_openssh() { encoding_test(OPENSSH_DSA_EXAMPLE) } -#[cfg(all(feature = "alloc", feature = "ecdsa"))] +#[cfg(all(feature = "alloc", feature = "p256"))] #[test] fn encode_ecdsa_p256_openssh() { encoding_test(OPENSSH_ECDSA_P256_EXAMPLE) } -#[cfg(all(feature = "alloc", feature = "ecdsa"))] +#[cfg(all(feature = "alloc", feature = "p384"))] #[test] fn encode_ecdsa_p384_openssh() { encoding_test(OPENSSH_ECDSA_P384_EXAMPLE) } -#[cfg(all(feature = "alloc", feature = "ecdsa"))] +#[cfg(all(feature = "alloc", feature = "p521"))] #[test] fn encode_ecdsa_p521_openssh() { encoding_test(OPENSSH_ECDSA_P521_EXAMPLE) From 1fe8fd1b8b40c085fa3db6442a5bda2c40c54b5a Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 29 Dec 2024 19:04:22 +0100 Subject: [PATCH 02/13] mac --- Cargo.lock | 92 +++++++++++++++++++++++++++++++++++++--------- ssh-key/Cargo.toml | 3 +- ssh-key/src/ppk.rs | 50 ++++++++++++++++++++++--- 3 files changed, 121 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 757cf4ed..7f575576 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.6.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5f451b77e2f92932dc411da6ef9f3d33efad68a6f14a7a83e559453458e85ac" dependencies = [ - "crypto-common", + "crypto-common 0.2.0-rc.0", ] [[package]] @@ -67,13 +67,22 @@ dependencies = [ "sha2", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.11.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17092d478f4fadfb35a7e082f62e49f0907fdf048801d9d706277e34f9df8a78" dependencies = [ - "crypto-common", + "crypto-common 0.2.0-rc.0", ] [[package]] @@ -140,7 +149,7 @@ version = "0.5.0-pre.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71c893d5a1e8257048dbb29954d2e1f85f091a150304f1defe4ca2806da5d3f" dependencies = [ - "crypto-common", + "crypto-common 0.2.0-rc.0", "inout", "zeroize", ] @@ -173,6 +182,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-common" version = "0.2.0-rc.0" @@ -201,7 +220,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest", + "digest 0.11.0-pre.9", "fiat-crypto", "rustc_version", "subtle", @@ -236,15 +255,26 @@ dependencies = [ "cipher", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.6", + "subtle", +] + [[package]] name = "digest" version = "0.11.0-pre.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf2e3d6615d99707295a9673e889bf363a04b2a466bd320c65a72536f7577379" dependencies = [ - "block-buffer", + "block-buffer 0.11.0-rc.0", "const-oid", - "crypto-common", + "crypto-common 0.2.0-rc.0", "subtle", ] @@ -254,7 +284,7 @@ version = "0.7.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28e0b1a3c7540d48f58eca5ddfdeb40a44aff3047bf15fe4fb6162a673ddd5fa" dependencies = [ - "digest", + "digest 0.11.0-pre.9", "num-bigint-dig", "num-traits", "pkcs8", @@ -271,7 +301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fad051af2b2d2f356d716138c76775929be913deb5b4ea217cd2613535936bef" dependencies = [ "der", - "digest", + "digest 0.11.0-pre.9", "elliptic-curve", "rfc6979", "signature", @@ -306,7 +336,7 @@ checksum = "4ed8e96bb573517f42470775f8ef1b9cd7595de52ba7a8e19c48325a92c8fe4f" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.11.0-pre.9", "ff", "group", "hybrid-array", @@ -333,6 +363,16 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -377,13 +417,22 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hmac" version = "0.13.0-pre.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4b1fb14e4df79f9406b434b60acef9f45c26c50062cccf1346c6103b8c47d58" dependencies = [ - "digest", + "digest 0.11.0-pre.9", ] [[package]] @@ -533,7 +582,7 @@ version = "0.13.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e11753d5193f26dc27ae698e0b536b5e511b7799c5ac475ec10783f26d164a" dependencies = [ - "digest", + "digest 0.11.0-pre.9", ] [[package]] @@ -664,7 +713,7 @@ version = "0.5.0-pre.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "871ee76a3eee98b0f805e5d1caf26929f4565073c580c053a55f886fc15dea49" dependencies = [ - "hmac", + "hmac 0.13.0-pre.4", "subtle", ] @@ -675,7 +724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07058e83b684989ab0559f9e22322f4e3f7e49147834ed0bae40486b9e70473c" dependencies = [ "const-oid", - "digest", + "digest 0.11.0-pre.9", "num-bigint-dig", "num-integer", "num-traits", @@ -746,7 +795,7 @@ checksum = "9540978cef7a8498211c1b1c14e5ce920fe5bd524ea84f4a3d72d4602515ae93" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.11.0-pre.9", ] [[package]] @@ -757,7 +806,7 @@ checksum = "540c0893cce56cdbcfebcec191ec8e0f470dd1889b6e7a0b503e310a94a168f5" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.11.0-pre.9", ] [[package]] @@ -766,7 +815,7 @@ version = "2.3.0-pre.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "054d71959c7051b9042c26af337f05cc930575ed2604d7d3ced3158383e59734" dependencies = [ - "digest", + "digest 0.11.0-pre.9", "rand_core", ] @@ -826,7 +875,7 @@ version = "0.3.0-pre.1" dependencies = [ "base64ct", "bytes", - "digest", + "digest 0.11.0-pre.9", "hex-literal", "pem-rfc7468", ] @@ -840,6 +889,7 @@ dependencies = [ "ed25519-dalek", "hex", "hex-literal", + "hmac 0.12.1", "home", "num-bigint-dig", "p256", @@ -903,10 +953,16 @@ version = "0.6.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3517d72c5ca6d60f9f2e85d2c772e2652830062a685105a528d19dd823cf87d5" dependencies = [ - "crypto-common", + "crypto-common 0.2.0-rc.0", "subtle", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index efbd5af5..3498d3ba 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -31,6 +31,7 @@ bigint = { package = "num-bigint-dig", version = "0.8", optional = true, default dsa = { version = "=0.7.0-pre.1", optional = true, default-features = false } ed25519-dalek = { version = "=2.2.0-pre", optional = true, default-features = false } hex = { version = "0.4", optional = true } +hmac = { version = "0.12", optional = true } home = { version = "0.5", optional = true } p256 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] } p384 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] } @@ -80,7 +81,7 @@ getrandom = ["rand_core/getrandom"] p256 = ["dep:p256", "ecdsa"] p384 = ["dep:p384", "ecdsa"] p521 = ["dep:p521", "ecdsa"] -ppk = ["dep:hex", "alloc", "std"] +ppk = ["dep:hex", "alloc", "std", "dep:hmac"] rsa = ["dep:bigint", "dep:rsa", "alloc", "rand_core"] tdes = ["cipher/tdes", "encryption"] diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs index 3a8465b7..9a085356 100644 --- a/ssh-key/src/ppk.rs +++ b/ssh-key/src/ppk.rs @@ -6,17 +6,20 @@ use core::fmt::{Debug, Display}; use core::num::ParseIntError; use core::str::FromStr; use hex::FromHex; +use hmac::{Hmac, Mac}; +use sha2::digest::Digest; +use sha2::Sha256; use std::collections::HashMap; use std::string::{String, ToString}; use std::vec::Vec; -use encoding::base64::{self, Base64, Encoding}; -use encoding::{Decode, LabelError, Reader}; - use crate::kdf::ArgonFlavor; use crate::private::{EcdsaKeypair, KeypairData}; use crate::public::KeyData; -use crate::{Algorithm, Error, Kdf, Mpint, PublicKey}; +use crate::{algorithm, Algorithm, Error, Kdf, Mpint, PublicKey}; +use encoding::base64::{self, Base64, Encoding}; +use encoding::{Decode, Encode, LabelError, Reader}; +use subtle::ConstantTimeEq; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum PpkEncryptionAlgorithm { @@ -106,6 +109,7 @@ pub enum PpkParseError { ValueFormat { key: PpkKey, value: String }, HexFormat(String), // FromHexError does not implement Eq InvalidInteger(ParseIntError), + IncorrectMac, UnknownKey(String), MissingValue(PpkKey), MissingPublicKey, @@ -128,6 +132,7 @@ impl Display for PpkParseError { } Self::HexFormat(err) => write!(f, "invalid hex format: {}", err), Self::InvalidInteger(err) => write!(f, "invalid integer: {}", err), + Self::IncorrectMac => write!(f, "incorrect MAC"), Self::UnknownKey(key) => write!(f, "unknown key: {:?}", key), Self::MissingValue(key) => write!(f, "missing value for key: {:?}", key), Self::MissingPublicKey => write!(f, "missing public key"), @@ -274,6 +279,42 @@ impl TryFrom for PpkContainer { let public_key = ppk.public_key.ok_or(PpkParseError::MissingPublicKey)?; let private_key = ppk.private_key.ok_or(PpkParseError::MissingPrivateKey)?; + let comment = ppk.values.remove(&PpkKey::Comment); + + let mac_buffer = { + let mut buf = vec![]; + ppk.algorithm.encode(&mut buf)?; + ppk.values + .get(&PpkKey::Encryption) + .map(String::as_bytes) + .unwrap_or_default() + .encode(&mut buf)?; + comment + .as_ref() + .map(String::as_bytes) + .unwrap_or_default() + .encode(&mut buf)?; + public_key.encode(&mut buf)?; + match encryption { + None => private_key.encode(&mut buf)?, + Some(_) => todo!(), + } + buf + }; + let hmac_key = match encryption { + None => [0; 64], + Some(_) => todo!(), + }; + + let expected_mac = { + let mut hmac = Hmac::::new(&hmac_key.try_into().unwrap()); //fixed length + hmac.update(&mac_buffer); + hmac.finalize() + }; + + if expected_mac.into_bytes().ct_ne(&mac).into() { + return Err(Error::Ppk(PpkParseError::IncorrectMac)); + } let mut public_key = PublicKey::from_bytes(&public_key)?; let mut private_key_cursor = &private_key[..]; @@ -284,7 +325,6 @@ impl TryFrom for PpkContainer { } }; - let comment = ppk.values.remove(&PpkKey::Comment); public_key.comment = comment.unwrap_or_default(); // todo verify mac From 78dd28cf13ff1fafdf3d2275ae1f788e63936604 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 29 Dec 2024 19:05:03 +0100 Subject: [PATCH 03/13] stuf --- Cargo.lock | 33 +++++++ ssh-key/Cargo.toml | 3 +- ssh-key/src/kdf.rs | 26 ----- ssh-key/src/ppk.rs | 183 +++++++++++++++++++++-------------- ssh-key/src/private.rs | 12 +-- ssh-key/tests/private_key.rs | 54 ++++++----- 6 files changed, 177 insertions(+), 134 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f575576..4f0bb560 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -67,6 +79,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -576,6 +597,17 @@ dependencies = [ "sha2", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "pbkdf2" version = "0.13.0-pre.1" @@ -884,6 +916,7 @@ dependencies = [ name = "ssh-key" version = "0.7.0-pre.1" dependencies = [ + "argon2", "bcrypt-pbkdf", "dsa", "ed25519-dalek", diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index 3498d3ba..f47ab8c1 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -26,6 +26,7 @@ subtle = { version = "2", default-features = false } zeroize = { version = "1", default-features = false } # optional dependencies +argon2 = { version = "0.5", optional = true, default-features = false, features = ["alloc"] } bcrypt-pbkdf = { version = "=0.11.0-pre.1", optional = true, default-features = false, features = ["alloc"] } bigint = { package = "num-bigint-dig", version = "0.8", optional = true, default-features = false } dsa = { version = "=0.7.0-pre.1", optional = true, default-features = false } @@ -81,7 +82,7 @@ getrandom = ["rand_core/getrandom"] p256 = ["dep:p256", "ecdsa"] p384 = ["dep:p384", "ecdsa"] p521 = ["dep:p521", "ecdsa"] -ppk = ["dep:hex", "alloc", "std", "dep:hmac"] +ppk = ["dep:hex", "alloc", "std", "dep:hmac", "dep:argon2", "encryption"] rsa = ["dep:bigint", "dep:rsa", "alloc", "rand_core"] tdes = ["cipher/tdes", "encryption"] diff --git a/ssh-key/src/kdf.rs b/ssh-key/src/kdf.rs index 6e744f62..9eb47e2d 100644 --- a/ssh-key/src/kdf.rs +++ b/ssh-key/src/kdf.rs @@ -19,14 +19,6 @@ const DEFAULT_BCRYPT_ROUNDS: u32 = 16; #[cfg(feature = "encryption")] const DEFAULT_SALT_SIZE: usize = 16; -#[cfg(feature = "ppk")] -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum ArgonFlavor { - I, - D, - ID, -} - /// Key Derivation Functions (KDF). #[derive(Clone, Debug, Eq, PartialEq)] #[non_exhaustive] @@ -43,16 +35,6 @@ pub enum Kdf { /// Rounds rounds: u32, }, - - /// Argon2 options. - #[cfg(feature = "ppk")] - Argon2 { - flavor: ArgonFlavor, - memory: u32, - passes: u32, - parallelism: u32, - salt: Vec, - }, } impl Kdf { @@ -80,8 +62,6 @@ impl Kdf { Self::None => KdfAlg::None, #[cfg(feature = "alloc")] Self::Bcrypt { .. } => KdfAlg::Bcrypt, - #[cfg(feature = "ppk")] - Self::Argon2 { .. } => todo!(), } } @@ -94,8 +74,6 @@ impl Kdf { bcrypt_pbkdf(password, salt, *rounds, output).map_err(|_| Error::Crypto)?; Ok(()) } - #[cfg(feature = "ppk")] - Self::Argon2 { .. } => todo!(), } } @@ -185,8 +163,6 @@ impl Encode for Kdf { Self::None => 4, #[cfg(feature = "alloc")] Self::Bcrypt { salt, .. } => [12, salt.len()].checked_sum()?, - #[cfg(feature = "ppk")] - Self::Argon2 { .. } => todo!(), }; [self.algorithm().encoded_len()?, kdfopts_prefixed_len].checked_sum() @@ -203,8 +179,6 @@ impl Encode for Kdf { salt.encode(writer)?; rounds.encode(writer)? } - #[cfg(feature = "ppk")] - Self::Argon2 { .. } => todo!(), } Ok(()) diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs index 9a085356..707879a5 100644 --- a/ssh-key/src/ppk.rs +++ b/ssh-key/src/ppk.rs @@ -1,55 +1,116 @@ // Format documentation: // https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html -use cipher::Cipher; +use argon2::Argon2; use core::fmt::{Debug, Display}; use core::num::ParseIntError; use core::str::FromStr; use hex::FromHex; use hmac::{Hmac, Mac}; -use sha2::digest::Digest; use sha2::Sha256; use std::collections::HashMap; use std::string::{String, ToString}; use std::vec::Vec; -use crate::kdf::ArgonFlavor; use crate::private::{EcdsaKeypair, KeypairData}; use crate::public::KeyData; -use crate::{algorithm, Algorithm, Error, Kdf, Mpint, PublicKey}; +use crate::{Algorithm, Error, Mpint, PublicKey}; use encoding::base64::{self, Base64, Encoding}; use encoding::{Decode, Encode, LabelError, Reader}; use subtle::ConstantTimeEq; -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum PpkEncryptionAlgorithm { - Aes256Cbc, +#[derive(Debug)] +pub enum Kdf { + Argon2 { kdf: Argon2<'static>, salt: Vec }, } -impl From for Cipher { - fn from(algorithm: PpkEncryptionAlgorithm) -> Self { - match algorithm { - PpkEncryptionAlgorithm::Aes256Cbc => Cipher::Aes256Cbc, +impl Kdf { + pub fn new(algorithm: &str, ppk: &PpkWrapper) -> Result { + let argon_algorithm = match algorithm { + "Argon2i" => Ok(argon2::Algorithm::Argon2i), + "Argon2d" => Ok(argon2::Algorithm::Argon2d), + "Argon2id" => Ok(argon2::Algorithm::Argon2id), + _ => Err(PpkParseError::UnsupportedKdf(algorithm.into())), + }?; + + let parse_int = |key: PpkKey| -> Result { + ppk.values + .get(&key) + .ok_or(PpkParseError::MissingValue(key)) + .and_then(|v| v.parse().map_err(PpkParseError::InvalidInteger)) + }; + + let argon = Argon2::new( + argon_algorithm, + argon2::Version::V0x13, + argon2::Params::new( + parse_int(PpkKey::Argon2Memory)?, + parse_int(PpkKey::Argon2Passes)?, + parse_int(PpkKey::Argon2Parallelism)?, + None, + ) + .map_err(PpkParseError::Argon2)?, + ); + + let salt = Vec::from_hex( + ppk.values + .get(&PpkKey::Argon2Salt) + .ok_or(PpkParseError::MissingValue(PpkKey::Argon2Salt))?, + ) + .map_err(|e| PpkParseError::HexFormat(e.to_string()))?; + + Ok(Self::Argon2 { kdf: argon, salt }) + } + + pub fn derive(&self, password: &[u8], output: &mut [u8]) -> Result<(), argon2::Error> { + match self { + Kdf::Argon2 { kdf, salt } => kdf.hash_password_into(password, salt, output), } } } -impl TryFrom<&str> for ArgonFlavor { - type Error = PpkParseError; +#[derive(Debug)] +pub enum Cipher { + Aes256Cbc, +} - fn try_from(value: &str) -> Result { - match value { - "Argon2i" => Ok(Self::I), - "Argon2d" => Ok(Self::D), - "Argon2id" => Ok(Self::ID), - _ => Err(PpkParseError::UnsupportedKdf(value.into())), +impl Cipher { + fn derive_aes_params( + kdf: &Kdf, + password: &str, + ) -> Result<([u8; 32], [u8; 16], [u8; 32]), Error> { + let mut key_iv_mac = vec![0; 80]; + kdf.derive(password.as_bytes(), &mut key_iv_mac) + .map_err(PpkParseError::Argon2)?; + let key = &key_iv_mac[..32]; + let iv = &key_iv_mac[32..48]; + let mac_key = &key_iv_mac[48..80]; + Ok(( + key.try_into().unwrap(), // const size + iv.try_into().unwrap(), // const size + mac_key.try_into().unwrap(), // const size + )) + } + + pub fn derive_mac_key(&self, kdf: &Kdf, password: &str) -> Result<[u8; 32], Error> { + Ok(Cipher::derive_aes_params(kdf, password)?.2) + } + + pub fn decrypt(&self, buf: &mut [u8], kdf: &Kdf, password: &str) -> Result<(), Error> { + let (key, iv, _) = Cipher::derive_aes_params(kdf, password)?; + match self { + Cipher::Aes256Cbc => cipher::Cipher::Aes256Cbc + .decrypt(&key, &iv, buf, None) + .map_err(Into::into), } } } +#[derive(Debug)] pub struct PpkEncryption { - pub algorithm: PpkEncryptionAlgorithm, + pub cipher: Cipher, pub kdf: Kdf, + pub passphrase: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -119,6 +180,8 @@ pub enum PpkParseError { UnsupportedFormatVersion(u8), UnsupportedEncryption(String), UnsupportedKdf(String), + Argon2(argon2::Error), + Encrypted, } impl Display for PpkParseError { @@ -146,6 +209,8 @@ impl Display for PpkParseError { write!(f, "unsupported encryption mode: {:?}", encryption) } Self::UnsupportedKdf(kdf) => write!(f, "unsupported KDF: {:?}", kdf), + Self::Argon2(err) => write!(f, "Argon2 error: {:?}", err), + Self::Encrypted => write!(f, "private key is encrypted"), } } } @@ -224,46 +289,28 @@ impl TryFrom<&str> for PpkWrapper { } } +#[derive(Debug)] pub struct PpkContainer { - pub version: u8, - pub encryption: Option, - pub mac: Vec, pub public_key: PublicKey, pub keypair_data: KeypairData, } -impl TryFrom for PpkContainer { - type Error = Error; - - fn try_from(mut ppk: PpkWrapper) -> Result { +impl PpkContainer { + pub fn new(mut ppk: PpkWrapper, passphrase: Option) -> Result { let encryption = match ppk.values.get(&PpkKey::Encryption).map(String::as_str) { None | Some("none") => None, Some("aes256-cbc") => { - let parse_int = |key: PpkKey| -> Result { - ppk.values - .get(&key) - .ok_or(PpkParseError::MissingValue(key)) - .and_then(|v| v.parse().map_err(PpkParseError::InvalidInteger)) + let Some(passphrase) = passphrase else { + return Err(PpkParseError::Encrypted.into()); }; - match ppk.values.get(&PpkKey::KeyDerivation).map(String::as_str) { None => { return Err(PpkParseError::MissingValue(PpkKey::KeyDerivation).into()); } Some(kdf) => Some(PpkEncryption { - algorithm: PpkEncryptionAlgorithm::Aes256Cbc, - kdf: Kdf::Argon2 { - flavor: ArgonFlavor::try_from(kdf)?, - memory: parse_int(PpkKey::Argon2Memory)?, - passes: parse_int(PpkKey::Argon2Passes)?, - parallelism: parse_int(PpkKey::Argon2Parallelism)?, - salt: Vec::from_hex( - ppk.values - .get(&PpkKey::Argon2Salt) - .ok_or(PpkParseError::MissingValue(PpkKey::Argon2Salt))?, - ) - .map_err(|e| PpkParseError::HexFormat(e.to_string()))?, - }, + kdf: Kdf::new(kdf, &ppk)?, + cipher: Cipher::Aes256Cbc, + passphrase, }), } } @@ -277,9 +324,14 @@ impl TryFrom for PpkContainer { ) .map_err(|e| PpkParseError::HexFormat(e.to_string()))?; - let public_key = ppk.public_key.ok_or(PpkParseError::MissingPublicKey)?; - let private_key = ppk.private_key.ok_or(PpkParseError::MissingPrivateKey)?; let comment = ppk.values.remove(&PpkKey::Comment); + let public_key = ppk.public_key.ok_or(PpkParseError::MissingPublicKey)?; + let mut private_key = ppk.private_key.ok_or(PpkParseError::MissingPrivateKey)?; + + if let Some(enc) = &encryption { + enc.cipher + .decrypt(&mut private_key, &enc.kdf, &enc.passphrase)?; + } let mac_buffer = { let mut buf = vec![]; @@ -295,19 +347,17 @@ impl TryFrom for PpkContainer { .unwrap_or_default() .encode(&mut buf)?; public_key.encode(&mut buf)?; - match encryption { - None => private_key.encode(&mut buf)?, - Some(_) => todo!(), - } + private_key.encode(&mut buf)?; buf }; - let hmac_key = match encryption { - None => [0; 64], - Some(_) => todo!(), + + let hmac_key = match &encryption { + None => [0; 32].into(), + Some(enc) => enc.cipher.derive_mac_key(&enc.kdf, &enc.passphrase)?, }; let expected_mac = { - let mut hmac = Hmac::::new(&hmac_key.try_into().unwrap()); //fixed length + let mut hmac = Hmac::::new_from_slice(&hmac_key).unwrap(); //fixed length hmac.update(&mac_buffer); hmac.finalize() }; @@ -318,21 +368,12 @@ impl TryFrom for PpkContainer { let mut public_key = PublicKey::from_bytes(&public_key)?; let mut private_key_cursor = &private_key[..]; - let keypair_data = match encryption { - Some(_) => todo!(), - None => { - decode_private_key_as(&mut private_key_cursor, public_key.clone(), ppk.algorithm)? - } - }; + let keypair_data = + decode_private_key_as(&mut private_key_cursor, public_key.clone(), ppk.algorithm)?; public_key.comment = comment.unwrap_or_default(); - // todo verify mac - Ok(PpkContainer { - version: ppk.version, - encryption, - mac, public_key, keypair_data, }) @@ -434,11 +475,3 @@ fn decode_private_key_as( _ => Err(algorithm.unsupported_error()), } } - -impl TryFrom<&str> for PpkContainer { - type Error = Error; - - fn try_from(contents: &str) -> Result { - PpkWrapper::try_from(contents.as_ref())?.try_into() - } -} diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index dbed9174..58f36d78 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -241,20 +241,16 @@ impl PrivateKey { /// PuTTY-User-Key-File-: /// ``` #[cfg(feature = "ppk")] - pub fn from_ppk(ppk: impl AsRef) -> Result { + pub fn from_ppk(ppk: impl AsRef, passphrase: Option) -> Result { use crate::ppk::PpkContainer; - let ppk: PpkContainer = PpkContainer::try_from(ppk.as_ref())?; + let ppk: PpkContainer = PpkContainer::new(ppk.as_ref().try_into()?, passphrase)?; Ok(Self { auth_tag: None, checkint: None, - cipher: ppk - .encryption - .as_ref() - .map(|e| e.algorithm.into()) - .unwrap_or(Cipher::None), - kdf: ppk.encryption.map(|x| x.kdf).unwrap_or(Kdf::None), + cipher: Cipher::None, + kdf: Kdf::None, key_data: ppk.keypair_data, public_key: ppk.public_key, }) diff --git a/ssh-key/tests/private_key.rs b/ssh-key/tests/private_key.rs index ed1ce9a4..4fa489ff 100644 --- a/ssh-key/tests/private_key.rs +++ b/ssh-key/tests/private_key.rs @@ -101,14 +101,14 @@ fn decode_dsa_openssh() { #[cfg(all(feature = "ppk", feature = "alloc"))] #[test] fn decode_dsa_ppk() { - validate_dsa(PrivateKey::from_ppk(PPK_DSA_EXAMPLE).unwrap()); + validate_dsa(PrivateKey::from_ppk(PPK_DSA_EXAMPLE, None).unwrap()); } -// #[cfg(all(feature = "ppk", feature = "alloc", feature = "encryption"))] -// #[test] -// fn decode_dsa_ppk_encrypted() { -// validate_dsa(PrivateKey::from_ppk(PPK_DSA_EXAMPLE_ENCRYPTED).unwrap()); -// } +#[cfg(all(feature = "ppk", feature = "alloc", feature = "encryption"))] +#[test] +fn decode_dsa_ppk_encrypted() { + validate_dsa(PrivateKey::from_ppk(PPK_DSA_EXAMPLE_ENCRYPTED, Some("123".into())).unwrap()); +} #[cfg(feature = "alloc")] fn validate_dsa(key: PrivateKey) { @@ -162,14 +162,16 @@ fn decode_ecdsa_p256_openssh() { #[cfg(all(feature = "ppk", feature = "p256"))] #[test] fn decode_ecdsa_p256_ppk() { - validate_ecdsa_p256(PrivateKey::from_ppk(PPK_ECDSA_P256_EXAMPLE).unwrap()); + validate_ecdsa_p256(PrivateKey::from_ppk(PPK_ECDSA_P256_EXAMPLE, None).unwrap()); } -// #[cfg(all(feature = "ppk", feature = "p256", feature = "encryption"))] -// #[test] -// fn decode_ecdsa_p256_ppk_encrypted() { -// validate_ecdsa_p256(PrivateKey::from_ppk(PPK_ECDSA_P256_EXAMPLE_ENCRYPTED).unwrap()); -// } +#[cfg(all(feature = "ppk", feature = "p256", feature = "encryption"))] +#[test] +fn decode_ecdsa_p256_ppk_encrypted() { + validate_ecdsa_p256( + PrivateKey::from_ppk(PPK_ECDSA_P256_EXAMPLE_ENCRYPTED, Some("123".into())).unwrap(), + ); +} fn validate_ecdsa_p256(key: PrivateKey) { assert_eq!( @@ -299,14 +301,16 @@ fn decode_ed25519_openssh() { #[test] #[cfg(all(feature = "ppk", feature = "ed25519"))] fn decode_ed25519_ppk() { - validate_ed25519(PrivateKey::from_ppk(PPK_ED25519_EXAMPLE).unwrap()); + validate_ed25519(PrivateKey::from_ppk(PPK_ED25519_EXAMPLE, None).unwrap()); } -// #[test] -// #[cfg(all(feature = "ppk", feature="ed25519",feature = "encryption"))] -// fn decode_ed25519_ppk_encrypted() { -// validate_ed25519(PrivateKey::from_ppk(PPK_ED25519_EXAMPLE_ENCRYPTED).unwrap()); -// } +#[test] +#[cfg(all(feature = "ppk", feature = "ed25519", feature = "encryption"))] +fn decode_ed25519_ppk_encrypted() { + validate_ed25519( + PrivateKey::from_ppk(PPK_ED25519_EXAMPLE_ENCRYPTED, Some("123".into())).unwrap(), + ); +} fn validate_ed25519(key: PrivateKey) { assert_eq!(Algorithm::Ed25519, key.algorithm()); @@ -345,14 +349,16 @@ fn decode_rsa_3072_openssh() { #[test] #[cfg(feature = "ppk")] fn decode_rsa_3072_ppk() { - validate_rsa_3072(PrivateKey::from_ppk(PPK_RSA_3072_EXAMPLE).unwrap()); + validate_rsa_3072(PrivateKey::from_ppk(PPK_RSA_3072_EXAMPLE, None).unwrap()); } -// #[test] -// #[cfg(feature = "ppk")] -// fn decode_rsa_3072_ppk_encrypted() { -// validate_rsa_3072(PrivateKey::from_ppk(PPK_RSA_3072_EXAMPLE_ENCRYPTED).unwrap()); -// } +#[test] +#[cfg(feature = "ppk")] +fn decode_rsa_3072_ppk_encrypted() { + validate_rsa_3072( + PrivateKey::from_ppk(PPK_RSA_3072_EXAMPLE_ENCRYPTED, Some("123".into())).unwrap(), + ); +} fn validate_rsa_3072(key: PrivateKey) { assert_eq!(Algorithm::Rsa { hash: None }, key.algorithm()); From 4f27cab8a338762880927e9a63a6809dd36b6247 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 29 Dec 2024 19:09:14 +0100 Subject: [PATCH 04/13] fixes --- Cargo.lock | 13 ++----------- ssh-key/Cargo.toml | 2 +- ssh-key/src/lib.rs | 4 ++-- ssh-key/src/ppk.rs | 22 ++++++++++------------ 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f0bb560..5196f923 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -438,15 +438,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "hmac" version = "0.13.0-pre.4" @@ -745,7 +736,7 @@ version = "0.5.0-pre.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "871ee76a3eee98b0f805e5d1caf26929f4565073c580c053a55f886fc15dea49" dependencies = [ - "hmac 0.13.0-pre.4", + "hmac", "subtle", ] @@ -922,7 +913,7 @@ dependencies = [ "ed25519-dalek", "hex", "hex-literal", - "hmac 0.12.1", + "hmac", "home", "num-bigint-dig", "p256", diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index f47ab8c1..30312b04 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -32,7 +32,7 @@ bigint = { package = "num-bigint-dig", version = "0.8", optional = true, default dsa = { version = "=0.7.0-pre.1", optional = true, default-features = false } ed25519-dalek = { version = "=2.2.0-pre", optional = true, default-features = false } hex = { version = "0.4", optional = true } -hmac = { version = "0.12", optional = true } +hmac = { version = "=0.13.0-pre.4", optional = true } home = { version = "0.5", optional = true } p256 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] } p384 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] } diff --git a/ssh-key/src/lib.rs b/ssh-key/src/lib.rs index 65d055c5..af231f7c 100644 --- a/ssh-key/src/lib.rs +++ b/ssh-key/src/lib.rs @@ -160,12 +160,12 @@ mod kdf; mod dot_ssh; #[cfg(feature = "alloc")] mod mpint; +#[cfg(feature = "ppk")] +mod ppk; #[cfg(feature = "alloc")] mod signature; #[cfg(feature = "alloc")] mod sshsig; -#[cfg(feature = "ppk")] -mod ppk; pub use crate::{ algorithm::{Algorithm, EcdsaCurve, HashAlg, KdfAlg}, diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs index 707879a5..3422c482 100644 --- a/ssh-key/src/ppk.rs +++ b/ssh-key/src/ppk.rs @@ -6,6 +6,7 @@ use core::fmt::{Debug, Display}; use core::num::ParseIntError; use core::str::FromStr; use hex::FromHex; +use hmac::digest::KeyInit; use hmac::{Hmac, Mac}; use sha2::Sha256; use std::collections::HashMap; @@ -175,7 +176,7 @@ pub enum PpkParseError { MissingValue(PpkKey), MissingPublicKey, MissingPrivateKey, - Base64(base64::InvalidEncodingError), + Base64(base64::Error), Eof, UnsupportedFormatVersion(u8), UnsupportedEncryption(String), @@ -263,8 +264,8 @@ impl TryFrom<&str> for PpkWrapper { content.extend_from_slice(line.as_bytes()); } - let decoded = - Base64::decode_in_place(&mut content).map_err(PpkParseError::Base64)?; + let decoded = Base64::decode_in_place(&mut content) + .map_err(|e| PpkParseError::Base64(e.into()))?; match key { "Public-Lines" => public_key = Some(decoded.to_vec()), @@ -388,10 +389,10 @@ fn decode_private_key_as( match (&algorithm, public.key_data()) { (Algorithm::Dsa { .. }, KeyData::Dsa(pk)) => { use crate::private::{DsaKeypair, DsaPrivateKey}; - Ok(KeypairData::Dsa(DsaKeypair { - private: DsaPrivateKey::decode(reader)?, - public: pk.clone(), - })) + Ok(KeypairData::Dsa(DsaKeypair::new( + pk.clone(), + DsaPrivateKey::decode(reader)?, + )?)) } #[cfg(feature = "rsa")] @@ -402,11 +403,8 @@ fn decode_private_key_as( let p = Mpint::decode(reader)?; let q = Mpint::decode(reader)?; let iqmp = Mpint::decode(reader)?; - let private = RsaPrivateKey { d, iqmp, p, q }; - Ok(KeypairData::Rsa(RsaKeypair { - private, - public: pk.clone(), - })) + let private = RsaPrivateKey::new(d, iqmp, p, q)?; + Ok(KeypairData::Rsa(RsaKeypair::new(pk.clone(), private)?)) } #[cfg(feature = "ed25519")] From 50672f8f14706326bec06717184e2e6725e17a9e Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 29 Dec 2024 19:12:32 +0100 Subject: [PATCH 05/13] fixes --- ssh-key/tests/private_key.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssh-key/tests/private_key.rs b/ssh-key/tests/private_key.rs index 4fa489ff..98293d80 100644 --- a/ssh-key/tests/private_key.rs +++ b/ssh-key/tests/private_key.rs @@ -220,7 +220,7 @@ fn decode_padless_wonder_openssh() { assert_eq!("", key.comment()); } -#[cfg(feature = "ecdsa")] +#[cfg(feature = "p384")] #[test] fn decode_ecdsa_p384_openssh() { let key = PrivateKey::from_openssh(OPENSSH_ECDSA_P384_EXAMPLE).unwrap(); From fe5e56a5c133f6a26cd64fe166183ca8321d94cf Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 29 Dec 2024 19:28:43 +0100 Subject: [PATCH 06/13] lint --- ssh-key/Cargo.toml | 2 +- ssh-key/src/ppk.rs | 65 +++++++++++++++++++++++++++------------------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index 30312b04..bd60a160 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -82,7 +82,7 @@ getrandom = ["rand_core/getrandom"] p256 = ["dep:p256", "ecdsa"] p384 = ["dep:p384", "ecdsa"] p521 = ["dep:p521", "ecdsa"] -ppk = ["dep:hex", "alloc", "std", "dep:hmac", "dep:argon2", "encryption"] +ppk = ["dep:hex", "alloc", "dep:hmac", "dep:argon2", "encryption"] rsa = ["dep:bigint", "dep:rsa", "alloc", "rand_core"] tdes = ["cipher/tdes", "encryption"] diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs index 3422c482..0635b427 100644 --- a/ssh-key/src/ppk.rs +++ b/ssh-key/src/ppk.rs @@ -1,6 +1,9 @@ // Format documentation: // https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html +use alloc::collections::BTreeMap; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; use argon2::Argon2; use core::fmt::{Debug, Display}; use core::num::ParseIntError; @@ -9,9 +12,6 @@ use hex::FromHex; use hmac::digest::KeyInit; use hmac::{Hmac, Mac}; use sha2::Sha256; -use std::collections::HashMap; -use std::string::{String, ToString}; -use std::vec::Vec; use crate::private::{EcdsaKeypair, KeypairData}; use crate::public::KeyData; @@ -75,11 +75,15 @@ pub enum Cipher { Aes256Cbc, } +type Aes256CbcKey = [u8; 32]; +type Aes256CbcIv = [u8; 16]; +type HmacKey = [u8; 32]; + impl Cipher { fn derive_aes_params( kdf: &Kdf, password: &str, - ) -> Result<([u8; 32], [u8; 16], [u8; 32]), Error> { + ) -> Result<(Aes256CbcKey, Aes256CbcIv, HmacKey), Error> { let mut key_iv_mac = vec![0; 80]; kdf.derive(password.as_bytes(), &mut key_iv_mac) .map_err(PpkParseError::Argon2)?; @@ -87,13 +91,16 @@ impl Cipher { let iv = &key_iv_mac[32..48]; let mac_key = &key_iv_mac[48..80]; Ok(( - key.try_into().unwrap(), // const size - iv.try_into().unwrap(), // const size - mac_key.try_into().unwrap(), // const size + #[allow(clippy::unwrap_used)] // const size + key.try_into().unwrap(), + #[allow(clippy::unwrap_used)] // const size + iv.try_into().unwrap(), + #[allow(clippy::unwrap_used)] // const size + mac_key.try_into().unwrap(), )) } - pub fn derive_mac_key(&self, kdf: &Kdf, password: &str) -> Result<[u8; 32], Error> { + pub fn derive_mac_key(&self, kdf: &Kdf, password: &str) -> Result { Ok(Cipher::derive_aes_params(kdf, password)?.2) } @@ -114,7 +121,7 @@ pub struct PpkEncryption { pub passphrase: String, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum PpkKey { Encryption, Comment, @@ -149,7 +156,7 @@ pub struct PpkWrapper { pub algorithm: Algorithm, pub public_key: Option>, pub private_key: Option>, - pub values: HashMap, + pub values: BTreeMap, } impl Debug for PpkWrapper { @@ -249,7 +256,7 @@ impl TryFrom<&str> for PpkWrapper { let mut public_key = None; let mut private_key = None; - let mut values = HashMap::new(); + let mut values = BTreeMap::new(); while let Some(line) = lines.next() { let (key, value) = line .split_once(": ") @@ -353,12 +360,13 @@ impl PpkContainer { }; let hmac_key = match &encryption { - None => [0; 32].into(), + None => HmacKey::default(), Some(enc) => enc.cipher.derive_mac_key(&enc.kdf, &enc.passphrase)?, }; let expected_mac = { - let mut hmac = Hmac::::new_from_slice(&hmac_key).unwrap(); //fixed length + #[allow(clippy::unwrap_used)] // const key length + let mut hmac = Hmac::::new_from_slice(&hmac_key).unwrap(); hmac.update(&mac_buffer); hmac.finalize() }; @@ -417,17 +425,22 @@ fn decode_private_key_as( let mut buf = Zeroizing::new([0u8; Ed25519PrivateKey::BYTE_SIZE]); let e = Mpint::decode(reader)?; let e_bytes = e.as_bytes(); - assert!(e_bytes.len() <= buf.len()); + + if e_bytes.len() > buf.len() { + return Err(Error::Crypto); + } + + #[allow(clippy::arithmetic_side_effects)] // length checked buf[Ed25519PrivateKey::BYTE_SIZE - e_bytes.len()..].copy_from_slice(e_bytes); let private = Ed25519PrivateKey::from_bytes(&buf); Ok(KeypairData::Ed25519(Ed25519Keypair { - public: pk.clone(), + public: *pk, private, })) } - #[cfg(feature = "ecdsa")] + #[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] (Algorithm::Ecdsa { curve }, KeyData::Ecdsa(public)) => { // PPK encodes EcDSA private exponent as an mpint use crate::public::EcdsaPublicKey; @@ -440,28 +453,28 @@ fn decode_private_key_as( return Err(Error::Crypto); } - type EC = EcdsaCurve; - type EPK = EcdsaPublicKey; - type EKP = EcdsaKeypair; + type Ec = EcdsaCurve; + type Epk = EcdsaPublicKey; + type Ekp = EcdsaKeypair; - let keypair: EKP = match (curve, public) { + let keypair: Ekp = match (curve, public) { #[cfg(feature = "p256")] - (EC::NistP256, EPK::NistP256(public)) => EKP::NistP256 { - public: public.clone(), + (Ec::NistP256, Epk::NistP256(public)) => Ekp::NistP256 { + public: *public, private: p256::SecretKey::from_slice(e_bytes) .map_err(|_| Error::Crypto)? .into(), }, #[cfg(feature = "p384")] - (EC::NistP384, EPK::NistP384(public)) => EKP::NistP384 { - public: public.clone(), + (Ec::NistP384, Epk::NistP384(public)) => Ekp::NistP384 { + public: *public, private: p384::SecretKey::from_slice(e_bytes) .map_err(|_| Error::Crypto)? .into(), }, #[cfg(feature = "p521")] - (EC::NistP521, EPK::NistP521(public)) => EKP::NistP521 { - public: public.clone(), + (Ec::NistP521, Epk::NistP521(public)) => Ekp::NistP521 { + public: *public, private: p521::SecretKey::from_slice(e_bytes) .map_err(|_| Error::Crypto)? .into(), From 64d5dd4a3dd0dd5ef966ead93ee9d893e652da66 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 29 Dec 2024 19:31:27 +0100 Subject: [PATCH 07/13] lint x2 --- ssh-key/src/ppk.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs index 0635b427..e3dda1b3 100644 --- a/ssh-key/src/ppk.rs +++ b/ssh-key/src/ppk.rs @@ -13,7 +13,7 @@ use hmac::digest::KeyInit; use hmac::{Hmac, Mac}; use sha2::Sha256; -use crate::private::{EcdsaKeypair, KeypairData}; +use crate::private::KeypairData; use crate::public::KeyData; use crate::{Algorithm, Error, Mpint, PublicKey}; use encoding::base64::{self, Base64, Encoding}; @@ -403,7 +403,6 @@ fn decode_private_key_as( )?)) } - #[cfg(feature = "rsa")] (Algorithm::Rsa { .. }, KeyData::Rsa(pk)) => { use crate::private::{RsaKeypair, RsaPrivateKey}; @@ -443,6 +442,7 @@ fn decode_private_key_as( #[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] (Algorithm::Ecdsa { curve }, KeyData::Ecdsa(public)) => { // PPK encodes EcDSA private exponent as an mpint + use crate::private::EcdsaKeypair; use crate::public::EcdsaPublicKey; use crate::EcdsaCurve; From 0f46ee4a714c7502b02e7b2a48ebbf8d5ee38569 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 29 Dec 2024 19:38:40 +0100 Subject: [PATCH 08/13] no_std fix --- .github/workflows/ssh-key.yml | 4 ++-- ssh-key/Cargo.toml | 2 +- ssh-key/src/ppk.rs | 1 + ssh-key/tests/private_key.rs | 2 ++ 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ssh-key.yml b/.github/workflows/ssh-key.yml index 5829969e..2709d75a 100644 --- a/.github/workflows/ssh-key.yml +++ b/.github/workflows/ssh-key.yml @@ -72,7 +72,7 @@ jobs: toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} - uses: RustCrypto/actions/cargo-hack-install@master - - run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features default,dsa,ed25519,getrandom,p256,p384,p521,rsa,tdes,std --release + - run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features default,dsa,ed25519,getrandom,p256,p384,p521,rsa,tdes,std,ppk --release - run: cargo build --target ${{ matrix.target }} --no-default-features --features alloc,crypto,dsa,encryption,tdes --release test: @@ -88,7 +88,7 @@ jobs: with: toolchain: ${{ matrix.rust }} - uses: RustCrypto/actions/cargo-hack-install@master - - run: cargo hack test --feature-powerset --exclude-features default,dsa,ed25519,getrandom,p256,p384,p521,rsa,tdes,std --release + - run: cargo hack test --feature-powerset --exclude-features default,dsa,ed25519,getrandom,p256,p384,p521,rsa,tdes,std,ppk --release - run: cargo test --release - run: cargo test --release --features getrandom - run: cargo test --release --features std diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index bd60a160..cf6d492d 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -31,7 +31,7 @@ bcrypt-pbkdf = { version = "=0.11.0-pre.1", optional = true, default-features = bigint = { package = "num-bigint-dig", version = "0.8", optional = true, default-features = false } dsa = { version = "=0.7.0-pre.1", optional = true, default-features = false } ed25519-dalek = { version = "=2.2.0-pre", optional = true, default-features = false } -hex = { version = "0.4", optional = true } +hex = { version = "0.4", optional = true, default-features = false, features = ["alloc"] } hmac = { version = "=0.13.0-pre.4", optional = true } home = { version = "0.5", optional = true } p256 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] } diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs index e3dda1b3..8a135d19 100644 --- a/ssh-key/src/ppk.rs +++ b/ssh-key/src/ppk.rs @@ -403,6 +403,7 @@ fn decode_private_key_as( )?)) } + #[cfg(feature = "rsa")] (Algorithm::Rsa { .. }, KeyData::Rsa(pk)) => { use crate::private::{RsaKeypair, RsaPrivateKey}; diff --git a/ssh-key/tests/private_key.rs b/ssh-key/tests/private_key.rs index 98293d80..09a350d9 100644 --- a/ssh-key/tests/private_key.rs +++ b/ssh-key/tests/private_key.rs @@ -173,6 +173,7 @@ fn decode_ecdsa_p256_ppk_encrypted() { ); } +#[cfg(feature = "p256")] fn validate_ecdsa_p256(key: PrivateKey) { assert_eq!( Algorithm::Ecdsa { @@ -360,6 +361,7 @@ fn decode_rsa_3072_ppk_encrypted() { ); } +#[cfg(feature = "rsa")] fn validate_rsa_3072(key: PrivateKey) { assert_eq!(Algorithm::Rsa { hash: None }, key.algorithm()); assert_eq!(Cipher::None, key.cipher()); From b900735748b7b4580b0f0ae769772236c5a7332f Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 29 Dec 2024 19:40:33 +0100 Subject: [PATCH 09/13] fixed tests --- ssh-key/tests/private_key.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ssh-key/tests/private_key.rs b/ssh-key/tests/private_key.rs index 09a350d9..1ebe8147 100644 --- a/ssh-key/tests/private_key.rs +++ b/ssh-key/tests/private_key.rs @@ -341,20 +341,20 @@ fn decode_ed25519_openssh_64cols() { assert_eq!(key, other_key); } -#[cfg(feature = "alloc")] +#[cfg(all(feature = "rsa", feature = "alloc"))] #[test] fn decode_rsa_3072_openssh() { validate_rsa_3072(PrivateKey::from_openssh(OPENSSH_RSA_3072_EXAMPLE).unwrap()); } #[test] -#[cfg(feature = "ppk")] +#[cfg(all(feature = "rsa", feature = "ppk"))] fn decode_rsa_3072_ppk() { validate_rsa_3072(PrivateKey::from_ppk(PPK_RSA_3072_EXAMPLE, None).unwrap()); } #[test] -#[cfg(feature = "ppk")] +#[cfg(all(feature = "rsa", feature = "ppk"))] fn decode_rsa_3072_ppk_encrypted() { validate_rsa_3072( PrivateKey::from_ppk(PPK_RSA_3072_EXAMPLE_ENCRYPTED, Some("123".into())).unwrap(), From 2efb1c6356b1551b79b34691edf5363171d16d49 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 29 Dec 2024 19:46:55 +0100 Subject: [PATCH 10/13] fixed tests again --- ssh-key/src/ppk.rs | 7 +++++-- ssh-key/tests/private_key.rs | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs index 8a135d19..5410ea69 100644 --- a/ssh-key/src/ppk.rs +++ b/ssh-key/src/ppk.rs @@ -15,7 +15,7 @@ use sha2::Sha256; use crate::private::KeypairData; use crate::public::KeyData; -use crate::{Algorithm, Error, Mpint, PublicKey}; +use crate::{Algorithm, Error, PublicKey}; use encoding::base64::{self, Base64, Encoding}; use encoding::{Decode, Encode, LabelError, Reader}; use subtle::ConstantTimeEq; @@ -397,6 +397,7 @@ fn decode_private_key_as( match (&algorithm, public.key_data()) { (Algorithm::Dsa { .. }, KeyData::Dsa(pk)) => { use crate::private::{DsaKeypair, DsaPrivateKey}; + Ok(KeypairData::Dsa(DsaKeypair::new( pk.clone(), DsaPrivateKey::decode(reader)?, @@ -406,6 +407,7 @@ fn decode_private_key_as( #[cfg(feature = "rsa")] (Algorithm::Rsa { .. }, KeyData::Rsa(pk)) => { use crate::private::{RsaKeypair, RsaPrivateKey}; + use crate::Mpint; let d = Mpint::decode(reader)?; let p = Mpint::decode(reader)?; @@ -419,6 +421,7 @@ fn decode_private_key_as( (Algorithm::Ed25519 { .. }, KeyData::Ed25519(pk)) => { // PPK encodes Ed25519 private exponent as an mpint use crate::private::{Ed25519Keypair, Ed25519PrivateKey}; + use crate::Mpint; use zeroize::Zeroizing; // Copy and pad exponent @@ -445,7 +448,7 @@ fn decode_private_key_as( // PPK encodes EcDSA private exponent as an mpint use crate::private::EcdsaKeypair; use crate::public::EcdsaPublicKey; - use crate::EcdsaCurve; + use crate::{EcdsaCurve, Mpint}; // Copy and pad exponent let e = Mpint::decode(reader)?; diff --git a/ssh-key/tests/private_key.rs b/ssh-key/tests/private_key.rs index 1ebe8147..a0730bd4 100644 --- a/ssh-key/tests/private_key.rs +++ b/ssh-key/tests/private_key.rs @@ -3,7 +3,7 @@ use hex_literal::hex; use ssh_key::{Algorithm, Cipher, KdfAlg, PrivateKey}; -#[cfg(feature = "ecdsa")] +#[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] use ssh_key::EcdsaCurve; #[cfg(feature = "alloc")] @@ -66,11 +66,11 @@ const OPENSSH_ED25519_64COLS_EXAMPLE: &str = include_str!("examples/id_ed25519.6 const OPENSSH_RSA_3072_EXAMPLE: &str = include_str!("examples/id_rsa_3072"); /// Same key, converted by puttygen -#[cfg(feature = "ppk")] +#[cfg(all(feature = "ppk", feature = "rsa"))] const PPK_RSA_3072_EXAMPLE: &str = include_str!("examples/id_rsa_3072.ppk"); /// Same key, converted and encrypted by puttygen -#[cfg(all(feature = "ppk", feature = "encryption"))] +#[cfg(all(feature = "ppk", feature = "rsa", feature = "encryption"))] const PPK_RSA_3072_EXAMPLE_ENCRYPTED: &str = include_str!("examples/id_rsa_3072_enc.ppk"); /// RSA (4096-bit) OpenSSH-formatted public key @@ -83,7 +83,7 @@ const OPENSSH_OPAQUE_EXAMPLE: &str = include_str!("examples/id_opaque"); /// OpenSSH-formatted private key with no internal or external padding, and no comment /// Trips a corner case in base64ct -#[cfg(feature = "ecdsa")] +#[cfg(feature = "p384")] const OPENSSH_PADLESS_WONDER_EXAMPLE: &str = include_str!("examples/padless_wonder"); /// Get a path into the `tests/scratch` directory. From ae169558fccb6e184179f5ac94161b5ae9d97bb4 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 29 Dec 2024 21:24:24 +0100 Subject: [PATCH 11/13] bumped argon2 --- Cargo.lock | 94 ++++++++++++---------------------------------- ssh-key/Cargo.toml | 2 +- 2 files changed, 25 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5196f923..bfe3e95e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.6.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5f451b77e2f92932dc411da6ef9f3d33efad68a6f14a7a83e559453458e85ac" dependencies = [ - "crypto-common 0.2.0-rc.0", + "crypto-common", ] [[package]] @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "argon2" -version = "0.5.3" +version = "0.6.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +checksum = "8f95281c5706985b6c00f8a2270438f968d475672aa68a4a85cddcb57a68577b" dependencies = [ "base64ct", "blake2", @@ -81,20 +81,11 @@ dependencies = [ [[package]] name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" +version = "0.11.0-pre.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "e6dbf347378982186052c47f25f33fc1a6eb439ee840d778eb3ec132e304379d" dependencies = [ - "generic-array", + "digest", ] [[package]] @@ -103,7 +94,7 @@ version = "0.11.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17092d478f4fadfb35a7e082f62e49f0907fdf048801d9d706277e34f9df8a78" dependencies = [ - "crypto-common 0.2.0-rc.0", + "crypto-common", ] [[package]] @@ -170,7 +161,7 @@ version = "0.5.0-pre.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71c893d5a1e8257048dbb29954d2e1f85f091a150304f1defe4ca2806da5d3f" dependencies = [ - "crypto-common 0.2.0-rc.0", + "crypto-common", "inout", "zeroize", ] @@ -203,16 +194,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.0-rc.0" @@ -241,7 +222,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest 0.11.0-pre.9", + "digest", "fiat-crypto", "rustc_version", "subtle", @@ -276,26 +257,15 @@ dependencies = [ "cipher", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.6", - "subtle", -] - [[package]] name = "digest" version = "0.11.0-pre.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf2e3d6615d99707295a9673e889bf363a04b2a466bd320c65a72536f7577379" dependencies = [ - "block-buffer 0.11.0-rc.0", + "block-buffer", "const-oid", - "crypto-common 0.2.0-rc.0", + "crypto-common", "subtle", ] @@ -305,7 +275,7 @@ version = "0.7.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28e0b1a3c7540d48f58eca5ddfdeb40a44aff3047bf15fe4fb6162a673ddd5fa" dependencies = [ - "digest 0.11.0-pre.9", + "digest", "num-bigint-dig", "num-traits", "pkcs8", @@ -322,7 +292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fad051af2b2d2f356d716138c76775929be913deb5b4ea217cd2613535936bef" dependencies = [ "der", - "digest 0.11.0-pre.9", + "digest", "elliptic-curve", "rfc6979", "signature", @@ -357,7 +327,7 @@ checksum = "4ed8e96bb573517f42470775f8ef1b9cd7595de52ba7a8e19c48325a92c8fe4f" dependencies = [ "base16ct", "crypto-bigint", - "digest 0.11.0-pre.9", + "digest", "ff", "group", "hybrid-array", @@ -384,16 +354,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -444,7 +404,7 @@ version = "0.13.0-pre.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4b1fb14e4df79f9406b434b60acef9f45c26c50062cccf1346c6103b8c47d58" dependencies = [ - "digest 0.11.0-pre.9", + "digest", ] [[package]] @@ -590,9 +550,9 @@ dependencies = [ [[package]] name = "password-hash" -version = "0.5.0" +version = "0.6.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +checksum = "ec3b470a56963403c40f9dbb41eaee539759de9d026d3324da705a0ae0d269cd" dependencies = [ "base64ct", "rand_core", @@ -605,7 +565,7 @@ version = "0.13.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e11753d5193f26dc27ae698e0b536b5e511b7799c5ac475ec10783f26d164a" dependencies = [ - "digest 0.11.0-pre.9", + "digest", ] [[package]] @@ -747,7 +707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07058e83b684989ab0559f9e22322f4e3f7e49147834ed0bae40486b9e70473c" dependencies = [ "const-oid", - "digest 0.11.0-pre.9", + "digest", "num-bigint-dig", "num-integer", "num-traits", @@ -818,7 +778,7 @@ checksum = "9540978cef7a8498211c1b1c14e5ce920fe5bd524ea84f4a3d72d4602515ae93" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.11.0-pre.9", + "digest", ] [[package]] @@ -829,7 +789,7 @@ checksum = "540c0893cce56cdbcfebcec191ec8e0f470dd1889b6e7a0b503e310a94a168f5" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.11.0-pre.9", + "digest", ] [[package]] @@ -838,7 +798,7 @@ version = "2.3.0-pre.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "054d71959c7051b9042c26af337f05cc930575ed2604d7d3ced3158383e59734" dependencies = [ - "digest 0.11.0-pre.9", + "digest", "rand_core", ] @@ -898,7 +858,7 @@ version = "0.3.0-pre.1" dependencies = [ "base64ct", "bytes", - "digest 0.11.0-pre.9", + "digest", "hex-literal", "pem-rfc7468", ] @@ -977,16 +937,10 @@ version = "0.6.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3517d72c5ca6d60f9f2e85d2c772e2652830062a685105a528d19dd823cf87d5" dependencies = [ - "crypto-common 0.2.0-rc.0", + "crypto-common", "subtle", ] -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index cf6d492d..5a869c5f 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -26,7 +26,7 @@ subtle = { version = "2", default-features = false } zeroize = { version = "1", default-features = false } # optional dependencies -argon2 = { version = "0.5", optional = true, default-features = false, features = ["alloc"] } +argon2 = { version = "=0.6.0-pre.1", optional = true, default-features = false, features = ["alloc"] } bcrypt-pbkdf = { version = "=0.11.0-pre.1", optional = true, default-features = false, features = ["alloc"] } bigint = { package = "num-bigint-dig", version = "0.8", optional = true, default-features = false } dsa = { version = "=0.7.0-pre.1", optional = true, default-features = false } From 98e181c2ad3e8fb778d7d7abdff3a6f53e803410 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 6 Jan 2025 20:11:41 +0100 Subject: [PATCH 12/13] ppk v2 format support --- ssh-key/Cargo.toml | 2 +- ssh-key/src/ppk.rs | 122 +++++++++++++++----- ssh-key/tests/examples/id_rsa_3072.ppk2 | 36 ++++++ ssh-key/tests/examples/id_rsa_3072_enc.ppk2 | 36 ++++++ ssh-key/tests/private_key.rs | 22 ++++ 5 files changed, 189 insertions(+), 29 deletions(-) create mode 100644 ssh-key/tests/examples/id_rsa_3072.ppk2 create mode 100644 ssh-key/tests/examples/id_rsa_3072_enc.ppk2 diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index 5a869c5f..991dff91 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -82,7 +82,7 @@ getrandom = ["rand_core/getrandom"] p256 = ["dep:p256", "ecdsa"] p384 = ["dep:p384", "ecdsa"] p521 = ["dep:p521", "ecdsa"] -ppk = ["dep:hex", "alloc", "dep:hmac", "dep:argon2", "encryption"] +ppk = ["dep:hex", "alloc", "cipher/aes-cbc", "dep:hmac", "dep:argon2", "dep:sha1"] rsa = ["dep:bigint", "dep:rsa", "alloc", "rand_core"] tdes = ["cipher/tdes", "encryption"] diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs index 5410ea69..b8df36a2 100644 --- a/ssh-key/src/ppk.rs +++ b/ssh-key/src/ppk.rs @@ -11,6 +11,7 @@ use core::str::FromStr; use hex::FromHex; use hmac::digest::KeyInit; use hmac::{Hmac, Mac}; +use sha1::{Digest, Sha1}; use sha2::Sha256; use crate::private::KeypairData; @@ -23,10 +24,15 @@ use subtle::ConstantTimeEq; #[derive(Debug)] pub enum Kdf { Argon2 { kdf: Argon2<'static>, salt: Vec }, + PpkV2, } impl Kdf { - pub fn new(algorithm: &str, ppk: &PpkWrapper) -> Result { + pub fn new_v2() -> Self { + Self::PpkV2 + } + + pub fn new_v3(algorithm: &str, ppk: &PpkWrapper) -> Result { let argon_algorithm = match algorithm { "Argon2i" => Ok(argon2::Algorithm::Argon2i), "Argon2d" => Ok(argon2::Algorithm::Argon2d), @@ -66,6 +72,7 @@ impl Kdf { pub fn derive(&self, password: &[u8], output: &mut [u8]) -> Result<(), argon2::Error> { match self { Kdf::Argon2 { kdf, salt } => kdf.hash_password_into(password, salt, output), + Kdf::PpkV2 => Ok(()), } } } @@ -77,27 +84,60 @@ pub enum Cipher { type Aes256CbcKey = [u8; 32]; type Aes256CbcIv = [u8; 16]; -type HmacKey = [u8; 32]; +type HmacKey = Vec; + +const PPK_V2_MAC_PREFIX: &str = "putty-private-key-file-mac-key"; impl Cipher { fn derive_aes_params( kdf: &Kdf, password: &str, ) -> Result<(Aes256CbcKey, Aes256CbcIv, HmacKey), Error> { - let mut key_iv_mac = vec![0; 80]; - kdf.derive(password.as_bytes(), &mut key_iv_mac) - .map_err(PpkParseError::Argon2)?; - let key = &key_iv_mac[..32]; - let iv = &key_iv_mac[32..48]; - let mac_key = &key_iv_mac[48..80]; - Ok(( - #[allow(clippy::unwrap_used)] // const size - key.try_into().unwrap(), - #[allow(clippy::unwrap_used)] // const size - iv.try_into().unwrap(), - #[allow(clippy::unwrap_used)] // const size - mac_key.try_into().unwrap(), - )) + match &kdf { + Kdf::Argon2 { .. } => { + let mut key_iv_mac = vec![0; 80]; + kdf.derive(password.as_bytes(), &mut key_iv_mac) + .map_err(PpkParseError::Argon2)?; + let key = &key_iv_mac[..32]; + let iv = &key_iv_mac[32..48]; + let mac_key = &key_iv_mac[48..80]; + Ok(( + #[allow(clippy::unwrap_used)] // const size + key.try_into().unwrap(), + #[allow(clippy::unwrap_used)] // const size + iv.try_into().unwrap(), + #[allow(clippy::unwrap_used)] // const size + mac_key.try_into().unwrap(), + )) + } + Kdf::PpkV2 => { + let mut hashes = { + let mut hash = Sha1::default(); + hash.update(&[0, 0, 0, 0]); + hash.update(password.as_bytes()); + hash.finalize().to_vec() + }; + hashes.extend_from_slice({ + let mut hash = Sha1::default(); + hash.update(&[0, 0, 0, 1]); + hash.update(password.as_bytes()); + hash.finalize().as_slice() + }); + + let aes_key = hashes[..32].try_into().unwrap(); + let iv = [0; 16]; + + let mac_key = { + let mut hash = Sha1::default(); + hash.update(PPK_V2_MAC_PREFIX.as_bytes()); + hash.update(password.as_bytes()); + hash.finalize() + } + .to_vec(); + + Ok((aes_key, iv, mac_key)) + } + } } pub fn derive_mac_key(&self, kdf: &Kdf, password: &str) -> Result { @@ -248,7 +288,7 @@ impl TryFrom<&str> for PpkWrapper { let version = header_version .parse() .map_err(|_| PpkParseError::Header(header.into()))?; - if version != 3 { + if version != 3 && version != 2 { return Err(PpkParseError::UnsupportedFormatVersion(version)); } @@ -311,15 +351,23 @@ impl PpkContainer { let Some(passphrase) = passphrase else { return Err(PpkParseError::Encrypted.into()); }; - match ppk.values.get(&PpkKey::KeyDerivation).map(String::as_str) { - None => { - return Err(PpkParseError::MissingValue(PpkKey::KeyDerivation).into()); - } - Some(kdf) => Some(PpkEncryption { - kdf: Kdf::new(kdf, &ppk)?, + match ppk.version { + 2 => Some(PpkEncryption { + kdf: Kdf::new_v2(), cipher: Cipher::Aes256Cbc, passphrase, }), + 3 => match ppk.values.get(&PpkKey::KeyDerivation).map(String::as_str) { + None => { + return Err(PpkParseError::MissingValue(PpkKey::KeyDerivation).into()); + } + Some(kdf) => Some(PpkEncryption { + kdf: Kdf::new_v3(kdf, &ppk)?, + cipher: Cipher::Aes256Cbc, + passphrase, + }), + }, + v => return Err(PpkParseError::UnsupportedFormatVersion(v).into()), } } Some(v) => return Err(PpkParseError::UnsupportedEncryption(v.into()).into()), @@ -360,18 +408,36 @@ impl PpkContainer { }; let hmac_key = match &encryption { - None => HmacKey::default(), + None => match ppk.version { + 2 => { + let mut hash = Sha1::new(); + hash.update(PPK_V2_MAC_PREFIX.as_bytes()); + hash.finalize().to_vec() + } + 3 => HmacKey::default(), + _ => unreachable!(), + }, Some(enc) => enc.cipher.derive_mac_key(&enc.kdf, &enc.passphrase)?, }; let expected_mac = { #[allow(clippy::unwrap_used)] // const key length - let mut hmac = Hmac::::new_from_slice(&hmac_key).unwrap(); - hmac.update(&mac_buffer); - hmac.finalize() + match ppk.version { + 3 => { + let mut hmac = Hmac::::new_from_slice(&hmac_key).unwrap(); + hmac.update(&mac_buffer); + hmac.finalize().into_bytes().to_vec() + } + 2 => { + let mut hmac = Hmac::::new_from_slice(&hmac_key).unwrap(); + hmac.update(&mac_buffer); + hmac.finalize().into_bytes().to_vec() + } + _ => unreachable!(), + } }; - if expected_mac.into_bytes().ct_ne(&mac).into() { + if expected_mac.ct_ne(&mac).into() { return Err(Error::Ppk(PpkParseError::IncorrectMac)); } diff --git a/ssh-key/tests/examples/id_rsa_3072.ppk2 b/ssh-key/tests/examples/id_rsa_3072.ppk2 new file mode 100644 index 00000000..7924f17e --- /dev/null +++ b/ssh-key/tests/examples/id_rsa_3072.ppk2 @@ -0,0 +1,36 @@ +PuTTY-User-Key-File-2: ssh-rsa +Encryption: none +Comment: user@example.com +Public-Lines: 9 +AAAAB3NzaC1yc2EAAAADAQABAAABgQCmjkeMm8k3JkNrf16eb5pG4bc77B6Mt3VN +4saltsRV8vASpyWa/PlBgdaeldOaNJ5NK0gqU3KyiUNzHbdcc8572e7IUBDJS/rl +aWARiSL4aos2VbNX0k56Z5zYp9m/bq5m9/mlb+PQkNBjIhimgpYNiq2TwBiYeA6t +Lb79cPtHA0cX5BLk/a5oUpLsiR4kI/f+Q98vVDKasKXXVh5YLkLobrruDB6er2A9 +fOcIUF0O4JCRLh/Dc161gE3fQrYTMQenbppZzfxrZfQ8YwLPvKjnqm+XRX+pbTta +Juj0EgTSzUK+EZxoSw8CNwiZpxrjwecTMVQ8w/srQmh4ABGuTqk0wP8HcI7hg+fp +Bv7kiejh5X/Oehxt+Puu85u9GVXb1a0av/vhJvUCBcuISvCA/z1wVJ0xdLhb1/Zi +TDdTzyNbZQ0OQijzK+e1SlkNhp+3eGVZu3pNZvnTppwIXv3wg6kV1HodkWGgh1ay +Y7Buc52Z8okDYqvJat5CzOj5OaQNr/k= +Private-Lines: 21 +AAABgGtjCxDGlQrA2fFicxA2JsOS3sB88gmKc9Ce6bOIzrgX5eAw8tcmSlOJMmaX +dZJUYMiiomnf2fDw/ZMoUsQCStyh3Ao9TUVsfr0RnwZPZEPE9jM3OGXkTAMx8Pfj +6Uo7Q6lSMx0OslUUObfhEQGy6qqagmXkEjekGNphx2XDRdA4dcsam3AXfC75Jo/p +rIxiwI+pFSp/4AzK3nKjrPbwBOW2F0JKgCeSLbwXXyKGJinkcnGYypQLO8JMkmjj +q19eWWW4OH4UcGebPqaAll+BWTyxQTENTEFWniWzdqLcTtkvkUm3XpcOgiRzCUbM +IPNR+BFbG7/Ls49r0GxiBHK3bWQdNYAq3vFSIKubKlfjWRj+J+E4EZzKVqmMzzwP +xoOhnychqHZuzdnFdndmJlbz0+BTJfP7NzJmI9u+xjs9mEgwst0nvrtr0u1TRd// +GN8YBq3rztqYRYBJaJMGgaw+UjE4xSFssTWZfj4UOngWrMPYdB6s7H4V9T2g8IEG +kXCNnQAAAMEA0R564khkDTsgKTaRiGVEzf4HeamqtWyPlia/HmZIv9mIvbCsfRGn +PjQFYzbUrTkA/3GE7kBLhLrrEaKjAvmC2U7vt1cDDsbXfZEV6u+Aq1dJoPW1kLKZ +/96U+ZMN7bqyrzMwlbCKUEubMPERLc5R837QDQQzQ9Qg0uL7iL1/iBt8iZDki5P9 +HShPzIwcB/vvwE0CklsvFZqan1Zwc+HJT9xuRy9IljvhbFxUU4Vq0r95FuQsNuda +UBiRDY2tA41zAAAAwQDL5Q5+zfXiyG52ypS+iwwFsJBB0rzd7rRnLnEg6syDgOXW +t3yFWDxQj47o1VfKvLbfroxyOF8PaTRevBWl3+yUnAdw0C15Rd01klYtpziGYuBT +xUVNJpDeKmPMVV4aAQ4toK4wfRwR+FKpx1aOAvk9SbKo+Se3mUOykgytMhqiCEEJ +0TbQhcHQXDn0w2z4n9w8ZqdV5j9EbhYwKxNZlADwqDMhoua5FT3wLwPeMY6gkDko +KFPyAR4JBdEVdmfK8eMAAADAVEBapmOunggANacQAvTDUdfQAsNSAHJebcD/bZAa +MEsQOi6gFlB5ltMZNYtb6k/rQJj1MFKPErmMUMfd/IX8Svkle6+apyNc30Z3NJt3 +5SpApeL0QSLRjOQJQZFOmRacSLcIiY0phpZWYHt+LrY1QeC71Wjk93S+wxN9AqWR +yMd7LhiN1vcu71z/GSfN5XOkyg1DwrbGqVchRFEi4c9qpfBbZcuchhJPn3n6KfBe +PwbzuD7cqZQfVxZQ4PtGiq5M +Private-MAC: c74d3a1f3aa0e626832ef79c401fb93831a7c7f5 diff --git a/ssh-key/tests/examples/id_rsa_3072_enc.ppk2 b/ssh-key/tests/examples/id_rsa_3072_enc.ppk2 new file mode 100644 index 00000000..f9183091 --- /dev/null +++ b/ssh-key/tests/examples/id_rsa_3072_enc.ppk2 @@ -0,0 +1,36 @@ +PuTTY-User-Key-File-2: ssh-rsa +Encryption: aes256-cbc +Comment: user@example.com +Public-Lines: 9 +AAAAB3NzaC1yc2EAAAADAQABAAABgQCmjkeMm8k3JkNrf16eb5pG4bc77B6Mt3VN +4saltsRV8vASpyWa/PlBgdaeldOaNJ5NK0gqU3KyiUNzHbdcc8572e7IUBDJS/rl +aWARiSL4aos2VbNX0k56Z5zYp9m/bq5m9/mlb+PQkNBjIhimgpYNiq2TwBiYeA6t +Lb79cPtHA0cX5BLk/a5oUpLsiR4kI/f+Q98vVDKasKXXVh5YLkLobrruDB6er2A9 +fOcIUF0O4JCRLh/Dc161gE3fQrYTMQenbppZzfxrZfQ8YwLPvKjnqm+XRX+pbTta +Juj0EgTSzUK+EZxoSw8CNwiZpxrjwecTMVQ8w/srQmh4ABGuTqk0wP8HcI7hg+fp +Bv7kiejh5X/Oehxt+Puu85u9GVXb1a0av/vhJvUCBcuISvCA/z1wVJ0xdLhb1/Zi +TDdTzyNbZQ0OQijzK+e1SlkNhp+3eGVZu3pNZvnTppwIXv3wg6kV1HodkWGgh1ay +Y7Buc52Z8okDYqvJat5CzOj5OaQNr/k= +Private-Lines: 21 +bRFyGRUcw1sl1Yip86E4zjIBt6Z0wLysSneBbUwqzzL7W+S2CNf/jljoVcWEGKnq +4lFz1HuCwZI7YXzm+RzLIukL+pL5oK3UC47zVbtuvVtI+Wn4x4t8BiB3KJ5GP5/o +VDLiph9qs+8Fi5a29hdK4vySwAUyD91rNmYON/bjNeVK8U0CGMnsyoSi+/e4fGJ/ +F5Roly2r/7SxxSQw+sqjnWAPX0Wqk11AsaU72shabZwJkv7Ro9D4a7alh52NMGzd +Cw7tyXU/z701cOyfjQSjLKmDkrHJqt4atIM4THMr+JJz4PVC/WQYnifeIxFGSWiu +SKXjCqnXjpeoDkqqee5gsmNrDK7Km0bzGGS7akvOQB8j8Rl5QL7Yu4ab1cdfciuI +3CUlF94SMwoR2bSxOkq12YnNTln4gnTIoGnpBfLstTa2OJFlzzU8OCaQOstrBCOy +xwS49/f5wl3lZIkaSGkahMAAas3egLk/NohPgFYqdrjqssUAChzZ8JgVVVhG2tbN +dhQgJqjCVm1YzIh27v+sNUviQhKGJ4jWuwT1bwdmFn1uW2mLgZuast9xcTiFXlNR +0ir5rAblRQPuRxjkpVrrsnYs6pHY/U6Q/5sS9zLz/ku5Z1yBrPX10I4d/t0QOfu1 +KphWOTuakUYWaEBhQcrOrISLH6MsAC/Tc8fHqATYOKaCQO/TE53euh8GikP2B5FJ +o2tQFteRtixgCN/fO+xwxyC0PdIteQgCcRNtiYLCOcoZvlsOGtEGNkgUjlOSRZ58 +9NB4OtQlBzUxRs7FvCmSUFwSO/azmJGdNIxEzTXinWnQ5qr8Y5cMU6wB+QUZZZxV +kjM/kHfP9eG1hapkIZaV18ql0Bu6nOoaP5oFTGPurMnkneoytL30qU6zr6hBCQs4 +SrXIo+WwraI8EJ1Rp6Fv2JNS+U50YGKTz3m93Eytus8KKsYQN1dJ9o8jPacXpJQE +BJTTeE38wn/+0SWuPeRBf+L7MYE00fKDQ89xHjFWAVoM1aty55nvfs2zVtDvsWen +SfJi1/3fSgbY07MY75nFGIT5dIj+EmFXCAPnMgTKUNvVK7hrcj0uBXDRdkKWpI2D +34XsD8oIc+QoGeG28cywfbJUveCnnehUCTrCJ0acIeh89v1jHTHMV8BF8WdBO5+i +UTkdNZsrWCqn76e1p9AOLRSwJMNgJIq71Ahdw2ynxb6NTxW85b3tMVMcT1G1X6Xv ++oAKPs9ru0hwZCeFJ7oUiuzOgpnKN2veKvcVk2x6KPojwpuBn1PCoqSgccNDE0RV +Ga59u+c7C0TK6WTfehxm12VnNf0PWObsZ0gA2ukwEpY= +Private-MAC: 967a19c88c37946ddb771c48ab5ad0e82159b47f diff --git a/ssh-key/tests/private_key.rs b/ssh-key/tests/private_key.rs index a0730bd4..8c86948e 100644 --- a/ssh-key/tests/private_key.rs +++ b/ssh-key/tests/private_key.rs @@ -73,6 +73,14 @@ const PPK_RSA_3072_EXAMPLE: &str = include_str!("examples/id_rsa_3072.ppk"); #[cfg(all(feature = "ppk", feature = "rsa", feature = "encryption"))] const PPK_RSA_3072_EXAMPLE_ENCRYPTED: &str = include_str!("examples/id_rsa_3072_enc.ppk"); +/// Same key, converted by puttygen +#[cfg(all(feature = "ppk", feature = "rsa"))] +const PPK_V2_RSA_3072_EXAMPLE: &str = include_str!("examples/id_rsa_3072.ppk2"); + +/// Same key, converted and encrypted by puttygen +#[cfg(all(feature = "ppk", feature = "rsa", feature = "encryption"))] +const PPK_V2_RSA_3072_EXAMPLE_ENCRYPTED: &str = include_str!("examples/id_rsa_3072_enc.ppk2"); + /// RSA (4096-bit) OpenSSH-formatted public key #[cfg(feature = "alloc")] const OPENSSH_RSA_4096_EXAMPLE: &str = include_str!("examples/id_rsa_4096"); @@ -361,6 +369,20 @@ fn decode_rsa_3072_ppk_encrypted() { ); } +#[test] +#[cfg(all(feature = "rsa", feature = "ppk"))] +fn decode_rsa_3072_ppk_v2() { + validate_rsa_3072(PrivateKey::from_ppk(PPK_V2_RSA_3072_EXAMPLE, None).unwrap()); +} + +#[test] +#[cfg(all(feature = "rsa", feature = "ppk"))] +fn decode_rsa_3072_ppk_v2_encrypted() { + validate_rsa_3072( + PrivateKey::from_ppk(PPK_V2_RSA_3072_EXAMPLE_ENCRYPTED, Some("123".into())).unwrap(), + ); +} + #[cfg(feature = "rsa")] fn validate_rsa_3072(key: PrivateKey) { assert_eq!(Algorithm::Rsa { hash: None }, key.algorithm()); From cb33ea45ca33d73212d501e5741b44cd454219b8 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 6 Jan 2025 20:22:13 +0100 Subject: [PATCH 13/13] lint --- ssh-key/src/ppk.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs index b8df36a2..a11c72ac 100644 --- a/ssh-key/src/ppk.rs +++ b/ssh-key/src/ppk.rs @@ -106,24 +106,24 @@ impl Cipher { key.try_into().unwrap(), #[allow(clippy::unwrap_used)] // const size iv.try_into().unwrap(), - #[allow(clippy::unwrap_used)] // const size - mac_key.try_into().unwrap(), + mac_key.into(), )) } Kdf::PpkV2 => { let mut hashes = { let mut hash = Sha1::default(); - hash.update(&[0, 0, 0, 0]); + hash.update([0, 0, 0, 0]); hash.update(password.as_bytes()); hash.finalize().to_vec() }; hashes.extend_from_slice({ let mut hash = Sha1::default(); - hash.update(&[0, 0, 0, 1]); + hash.update([0, 0, 0, 1]); hash.update(password.as_bytes()); hash.finalize().as_slice() }); + #[allow(clippy::unwrap_used)] // known size let aes_key = hashes[..32].try_into().unwrap(); let iv = [0; 16];