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/Cargo.lock b/Cargo.lock index 375f7079..1ae2baac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "argon2" +version = "0.6.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f95281c5706985b6c00f8a2270438f968d475672aa68a4a85cddcb57a68577b" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -67,6 +79,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "blake2" +version = "0.11.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6dbf347378982186052c47f25f33fc1a6eb439ee840d778eb3ec132e304379d" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.11.0-rc.0" @@ -365,6 +386,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" @@ -521,6 +548,17 @@ dependencies = [ "sha2", ] +[[package]] +name = "password-hash" +version = "0.6.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3b470a56963403c40f9dbb41eaee539759de9d026d3324da705a0ae0d269cd" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "pbkdf2" version = "0.13.0-pre.1" @@ -829,10 +867,13 @@ dependencies = [ name = "ssh-key" version = "0.7.0-pre.1" dependencies = [ + "argon2", "bcrypt-pbkdf", "dsa", "ed25519-dalek", + "hex", "hex-literal", + "hmac", "home", "num-bigint-dig", "p256", diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index 389e2662..991dff91 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -26,10 +26,13 @@ subtle = { version = "2", default-features = false } zeroize = { version = "1", default-features = false } # optional dependencies +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 } ed25519-dalek = { version = "=2.2.0-pre", optional = true, default-features = false } +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"] } p384 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] } @@ -79,6 +82,7 @@ getrandom = ["rand_core/getrandom"] p256 = ["dep:p256", "ecdsa"] p384 = ["dep:p384", "ecdsa"] p521 = ["dep:p521", "ecdsa"] +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/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/lib.rs b/ssh-key/src/lib.rs index 56acda8e..af231f7c 100644 --- a/ssh-key/src/lib.rs +++ b/ssh-key/src/lib.rs @@ -160,6 +160,8 @@ mod kdf; mod dot_ssh; #[cfg(feature = "alloc")] mod mpint; +#[cfg(feature = "ppk")] +mod ppk; #[cfg(feature = "alloc")] mod signature; #[cfg(feature = "alloc")] diff --git a/ssh-key/src/ppk.rs b/ssh-key/src/ppk.rs new file mode 100644 index 00000000..a11c72ac --- /dev/null +++ b/ssh-key/src/ppk.rs @@ -0,0 +1,558 @@ +// 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; +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; +use crate::public::KeyData; +use crate::{Algorithm, Error, PublicKey}; +use encoding::base64::{self, Base64, Encoding}; +use encoding::{Decode, Encode, LabelError, Reader}; +use subtle::ConstantTimeEq; + +#[derive(Debug)] +pub enum Kdf { + Argon2 { kdf: Argon2<'static>, salt: Vec }, + PpkV2, +} + +impl Kdf { + 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), + "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), + Kdf::PpkV2 => Ok(()), + } + } +} + +#[derive(Debug)] +pub enum Cipher { + Aes256Cbc, +} + +type Aes256CbcKey = [u8; 32]; +type Aes256CbcIv = [u8; 16]; +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> { + 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(), + mac_key.into(), + )) + } + 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() + }); + + #[allow(clippy::unwrap_used)] // known size + 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 { + 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 cipher: Cipher, + pub kdf: Kdf, + pub passphrase: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +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: BTreeMap, +} + +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), + IncorrectMac, + UnknownKey(String), + MissingValue(PpkKey), + MissingPublicKey, + MissingPrivateKey, + Base64(base64::Error), + Eof, + UnsupportedFormatVersion(u8), + UnsupportedEncryption(String), + UnsupportedKdf(String), + Argon2(argon2::Error), + Encrypted, +} + +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::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"), + 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), + Self::Argon2(err) => write!(f, "Argon2 error: {:?}", err), + Self::Encrypted => write!(f, "private key is encrypted"), + } + } +} + +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 && version != 2 { + 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 = BTreeMap::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(|e| PpkParseError::Base64(e.into()))?; + + 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, + }) + } +} + +#[derive(Debug)] +pub struct PpkContainer { + pub public_key: PublicKey, + pub keypair_data: KeypairData, +} + +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 Some(passphrase) = passphrase else { + return Err(PpkParseError::Encrypted.into()); + }; + 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()), + }; + + 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 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![]; + 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)?; + private_key.encode(&mut buf)?; + buf + }; + + let hmac_key = match &encryption { + 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 + 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.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[..]; + let keypair_data = + decode_private_key_as(&mut private_key_cursor, public_key.clone(), ppk.algorithm)?; + + public_key.comment = comment.unwrap_or_default(); + + Ok(PpkContainer { + 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::new( + pk.clone(), + DsaPrivateKey::decode(reader)?, + )?)) + } + + #[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)?; + let q = Mpint::decode(reader)?; + let iqmp = Mpint::decode(reader)?; + let private = RsaPrivateKey::new(d, iqmp, p, q)?; + Ok(KeypairData::Rsa(RsaKeypair::new(pk.clone(), private)?)) + } + + #[cfg(feature = "ed25519")] + (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 + let mut buf = Zeroizing::new([0u8; Ed25519PrivateKey::BYTE_SIZE]); + let e = Mpint::decode(reader)?; + let e_bytes = e.as_bytes(); + + 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, + private, + })) + } + + #[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, Mpint}; + + // 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, + private: p256::SecretKey::from_slice(e_bytes) + .map_err(|_| Error::Crypto)? + .into(), + }, + #[cfg(feature = "p384")] + (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, + private: p521::SecretKey::from_slice(e_bytes) + .map_err(|_| Error::Crypto)? + .into(), + }, + _ => return Err(Error::Crypto), + }; + Ok(keypair.into()) + } + _ => Err(algorithm.unsupported_error()), + } +} diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index 311a1fe2..58f36d78 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -233,6 +233,29 @@ 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, passphrase: Option) -> Result { + use crate::ppk::PpkContainer; + + let ppk: PpkContainer = PpkContainer::new(ppk.as_ref().try_into()?, passphrase)?; + + Ok(Self { + auth_tag: None, + checkint: None, + cipher: Cipher::None, + kdf: 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.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.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/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 a6b3eb25..8c86948e 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")] @@ -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,22 @@ 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(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 = "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"); @@ -51,7 +91,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. @@ -63,7 +103,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, 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, Some("123".into())).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 +161,28 @@ 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, 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, Some("123".into())).unwrap(), + ); +} + +#[cfg(feature = "p256")] +fn validate_ecdsa_p256(key: PrivateKey) { assert_eq!( Algorithm::Ecdsa { curve: EcdsaCurve::NistP256 @@ -137,7 +211,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(); @@ -155,7 +229,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(); @@ -191,7 +265,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 +304,24 @@ 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, 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, Some("123".into())).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()); @@ -258,10 +349,42 @@ 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() { - let key = PrivateKey::from_openssh(OPENSSH_RSA_3072_EXAMPLE).unwrap(); + validate_rsa_3072(PrivateKey::from_openssh(OPENSSH_RSA_3072_EXAMPLE).unwrap()); +} + +#[test] +#[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(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(), + ); +} + +#[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()); assert_eq!(Cipher::None, key.cipher()); assert_eq!(KdfAlg::None, key.kdf().algorithm()); @@ -457,19 +580,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)