Skip to main content

bssh_russh/keys/
mod.rs

1//! This crate contains methods to deal with SSH keys, as defined in
2//! crate Russh. This includes in particular various functions for
3//! opening key files, deciphering encrypted keys, and dealing with
4//! agents.
5//!
6//! The following example shows how to do all these in a single example:
7//! start and SSH agent server, connect to it with a client, decipher
8//! an encrypted ED25519 private key (the password is `b"blabla"`), send it to
9//! the agent, and ask the agent to sign a piece of data
10//! (`b"Please sign this"`, below).
11//!
12//!```
13//! use russh::keys::*;
14//! use futures::Future;
15//!
16//! #[derive(Clone)]
17//! struct X{}
18//! impl agent::server::Agent for X {
19//!     fn confirm(self, _: std::sync::Arc<PrivateKey>) -> Box<dyn Future<Output = (Self, bool)> + Send + Unpin> {
20//!         Box::new(futures::future::ready((self, true)))
21//!     }
22//! }
23//!
24//! const PKCS8_ENCRYPTED: &'static str = "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIGjMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBAWQiUHKoocuxfoZ/hF\nYTjkAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQ83d1d5/S2wz475uC\nCUrE7QRAvdVpD5e3zKH/MZjilWrMOm6cyI1LKBCssLztPyvOALtroLAPlp7WYWfu\n9Sncmm7u14n2lia7r1r5I3VBsVuH0g==\n-----END ENCRYPTED PRIVATE KEY-----\n";
25//!
26//! #[cfg(unix)]
27//! fn main() {
28//!    env_logger::try_init().unwrap_or(());
29//!    let dir = tempfile::tempdir().unwrap();
30//!    let agent_path = dir.path().join("agent");
31//!
32//!    let mut core = tokio::runtime::Runtime::new().unwrap();
33//!    let agent_path_ = agent_path.clone();
34//!    // Starting a server
35//!    core.spawn(async move {
36//!        let mut listener = tokio::net::UnixListener::bind(&agent_path_)
37//!            .unwrap();
38//!        russh::keys::agent::server::serve(tokio_stream::wrappers::UnixListenerStream::new(listener), X {}).await
39//!    });
40//!    let key = decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap();
41//!    let public = key.public_key().clone();
42//!    core.block_on(async move {
43//!        let stream = tokio::net::UnixStream::connect(&agent_path).await?;
44//!        let mut client = agent::client::AgentClient::connect(stream);
45//!        client.add_identity(&key, &[agent::Constraint::KeyLifetime { seconds: 60 }]).await?;
46//!        client.request_identities().await?;
47//!        let buf = b"signed message".to_vec();
48//!        let sig = client.sign_request(&public.into(), None, buf).await.unwrap();
49//!        // Here, `sig` is encoded in a format usable internally by the SSH protocol.
50//!        Ok::<(), Error>(())
51//!    }).unwrap()
52//! }
53//!
54//! #[cfg(not(unix))]
55//! fn main() {}
56//!
57//! ```
58
59use std::fs::File;
60use std::io::Read;
61use std::path::Path;
62use std::string::FromUtf8Error;
63
64use aes::cipher::block_padding::UnpadError;
65use aes::cipher::inout::PadError;
66use data_encoding::BASE64_MIME;
67use thiserror::Error;
68
69use crate::helpers::EncodedExt;
70
71pub mod key;
72pub use key::PrivateKeyWithHashAlg;
73
74mod format;
75pub use format::*;
76// Reexports
77pub use signature;
78pub use ssh_encoding;
79pub use ssh_key::{self, Algorithm, Certificate, EcdsaCurve, HashAlg, PrivateKey, PublicKey};
80
81/// OpenSSH agent protocol implementation
82pub mod agent;
83
84#[cfg(not(target_arch = "wasm32"))]
85pub mod known_hosts;
86
87#[cfg(not(target_arch = "wasm32"))]
88pub use known_hosts::{check_known_hosts, check_known_hosts_path};
89
90#[derive(Debug, Error)]
91pub enum Error {
92    /// The key could not be read, for an unknown reason
93    #[error("Could not read key")]
94    CouldNotReadKey,
95    /// The type of the key is unsupported
96    #[error("Unsupported key type {}", key_type_string)]
97    UnsupportedKeyType {
98        key_type_string: String,
99        key_type_raw: Vec<u8>,
100    },
101    /// The type of the key is unsupported
102    #[error("Invalid Ed25519 key data")]
103    Ed25519KeyError(#[from] ed25519_dalek::SignatureError),
104    /// The type of the key is unsupported
105    #[error("Invalid ECDSA key data")]
106    EcdsaKeyError(#[from] p256::elliptic_curve::Error),
107    /// The key is encrypted (should supply a password?)
108    #[error("The key is encrypted")]
109    KeyIsEncrypted,
110    /// The key contents are inconsistent
111    #[error("The key is corrupt")]
112    KeyIsCorrupt,
113    /// Home directory could not be found
114    #[error("No home directory found")]
115    NoHomeDir,
116    /// The server key has changed
117    #[error("The server key changed at line {}", line)]
118    KeyChanged { line: usize },
119    /// The key uses an unsupported algorithm
120    #[error("Unknown key algorithm: {0}")]
121    UnknownAlgorithm(::pkcs8::ObjectIdentifier),
122    /// Index out of bounds
123    #[error("Index out of bounds")]
124    IndexOutOfBounds,
125    /// Unknown signature type
126    #[error("Unknown signature type: {}", sig_type)]
127    UnknownSignatureType { sig_type: String },
128    #[error("Invalid signature")]
129    InvalidSignature,
130    #[error("Invalid parameters")]
131    InvalidParameters,
132    /// Agent protocol error
133    #[error("Agent protocol error")]
134    AgentProtocolError,
135    #[error("Agent failure")]
136    AgentFailure,
137    #[error(transparent)]
138    IO(#[from] std::io::Error),
139
140    #[cfg(feature = "rsa")]
141    #[error("Rsa: {0}")]
142    Rsa(#[from] rsa::Error),
143
144    #[error(transparent)]
145    Pad(#[from] PadError),
146
147    #[error(transparent)]
148    Unpad(#[from] UnpadError),
149
150    #[error("Base64 decoding error: {0}")]
151    Decode(#[from] data_encoding::DecodeError),
152    #[error("Der: {0}")]
153    Der(#[from] der::Error),
154    #[error("Spki: {0}")]
155    Spki(#[from] spki::Error),
156    #[cfg(feature = "rsa")]
157    #[error("Pkcs1: {0}")]
158    Pkcs1(#[from] pkcs1::Error),
159    #[error("Pkcs8: {0}")]
160    Pkcs8(#[from] ::pkcs8::Error),
161    #[error("Sec1: {0}")]
162    Sec1(#[from] sec1::Error),
163
164    #[error("SshKey: {0}")]
165    SshKey(#[from] ssh_key::Error),
166    #[error("SshEncoding: {0}")]
167    SshEncoding(#[from] ssh_encoding::Error),
168
169    #[error("Environment variable `{0}` not found")]
170    EnvVar(&'static str),
171    #[error(
172        "Unable to connect to ssh-agent. The environment variable `SSH_AUTH_SOCK` was set, but it \
173         points to a nonexistent file or directory."
174    )]
175    BadAuthSock,
176
177    #[error(transparent)]
178    Utf8(#[from] FromUtf8Error),
179
180    #[error("ASN1 decoding error: {0}")]
181    #[cfg(feature = "legacy-ed25519-pkcs8-parser")]
182    LegacyASN1(::yasna::ASN1Error),
183
184    #[cfg(windows)]
185    #[error("Pageant: {0}")]
186    Pageant(#[from] pageant::Error),
187}
188
189#[cfg(feature = "legacy-ed25519-pkcs8-parser")]
190impl From<yasna::ASN1Error> for Error {
191    fn from(e: yasna::ASN1Error) -> Error {
192        Error::LegacyASN1(e)
193    }
194}
195
196/// Load a public key from a file. Ed25519, EC-DSA and RSA keys are supported.
197///
198/// ```
199/// russh::keys::load_public_key("../files/id_ed25519.pub").unwrap();
200/// ```
201pub fn load_public_key<P: AsRef<Path>>(path: P) -> Result<ssh_key::PublicKey, Error> {
202    let mut pubkey = String::new();
203    let mut file = File::open(path.as_ref())?;
204    file.read_to_string(&mut pubkey)?;
205
206    let mut split = pubkey.split_whitespace();
207    match (split.next(), split.next()) {
208        (Some(_), Some(key)) => parse_public_key_base64(key),
209        (Some(key), None) => parse_public_key_base64(key),
210        _ => Err(Error::CouldNotReadKey),
211    }
212}
213
214/// Reads a public key from the standard encoding. In some cases, the
215/// encoding is prefixed with a key type identifier and a space (such
216/// as `ssh-ed25519 AAAAC3N...`).
217///
218/// ```
219/// russh::keys::parse_public_key_base64("AAAAC3NzaC1lZDI1NTE5AAAAIJdD7y3aLq454yWBdwLWbieU1ebz9/cu7/QEXn9OIeZJ").is_ok();
220/// ```
221pub fn parse_public_key_base64(key: &str) -> Result<ssh_key::PublicKey, Error> {
222    let base = BASE64_MIME.decode(key.as_bytes())?;
223    key::parse_public_key(&base)
224}
225
226pub trait PublicKeyBase64 {
227    /// Create the base64 part of the public key blob.
228    fn public_key_bytes(&self) -> Vec<u8>;
229    fn public_key_base64(&self) -> String {
230        let mut s = BASE64_MIME.encode(&self.public_key_bytes());
231        assert_eq!(s.pop(), Some('\n'));
232        assert_eq!(s.pop(), Some('\r'));
233        s.replace("\r\n", "")
234    }
235}
236
237impl PublicKeyBase64 for ssh_key::PublicKey {
238    fn public_key_bytes(&self) -> Vec<u8> {
239        self.key_data().encoded().unwrap_or_default()
240    }
241}
242
243impl PublicKeyBase64 for PrivateKey {
244    fn public_key_bytes(&self) -> Vec<u8> {
245        self.public_key().public_key_bytes()
246    }
247}
248
249/// Load a secret key, deciphering it with the supplied password if necessary.
250pub fn load_secret_key<P: AsRef<Path>>(
251    secret_: P,
252    password: Option<&str>,
253) -> Result<PrivateKey, Error> {
254    let mut secret_file = std::fs::File::open(secret_)?;
255    let mut secret = String::new();
256    secret_file.read_to_string(&mut secret)?;
257    decode_secret_key(&secret, password)
258}
259
260/// Load a openssh certificate
261pub fn load_openssh_certificate<P: AsRef<Path>>(cert_: P) -> Result<Certificate, ssh_key::Error> {
262    let mut cert_file = std::fs::File::open(cert_)?;
263    let mut cert = String::new();
264    cert_file.read_to_string(&mut cert)?;
265
266    Certificate::from_openssh(&cert)
267}
268
269fn is_base64_char(c: char) -> bool {
270    c.is_ascii_lowercase()
271        || c.is_ascii_uppercase()
272        || c.is_ascii_digit()
273        || c == '/'
274        || c == '+'
275        || c == '='
276}
277
278#[cfg(test)]
279mod test {
280
281    #[cfg(unix)]
282    use futures::Future;
283
284    use super::*;
285    #[cfg(unix)]
286    use crate::keys::agent::AgentIdentity;
287    use crate::keys::key::PublicKeyExt;
288
289    const ED25519_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY-----
290b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABDLGyfA39
291J2FcJygtYqi5ISAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIN+Wjn4+4Fcvl2Jl
292KpggT+wCRxpSvtqqpVrQrKN1/A22AAAAkOHDLnYZvYS6H9Q3S3Nk4ri3R2jAZlQlBbUos5
293FkHpYgNw65KCWCTXtP7ye2czMC3zjn2r98pJLobsLYQgRiHIv/CUdAdsqbvMPECB+wl/UQ
294e+JpiSq66Z6GIt0801skPh20jxOO3F52SoX1IeO5D5PXfZrfSZlw6S8c7bwyp2FHxDewRx
2957/wNsnDM0T7nLv/Q==
296-----END OPENSSH PRIVATE KEY-----";
297
298    // password is 'test'
299    const ED25519_AESCTR_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY-----
300b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABD1phlku5
301A2G7Q9iP+DcOc9AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHeLC1lWiCYrXsf/
30285O/pkbUFZ6OGIt49PX3nw8iRoXEAAAAkKRF0st5ZI7xxo9g6A4m4l6NarkQre3mycqNXQ
303dP3jryYgvsCIBAA5jMWSjrmnOTXhidqcOy4xYCrAttzSnZ/cUadfBenL+DQq6neffw7j8r
3040tbCxVGp6yCQlKrgSZf6c0Hy7dNEIU2bJFGxLe6/kWChcUAt/5Ll5rI7DVQPJdLgehLzvv
305sJWR7W+cGvJ/vLsw==
306-----END OPENSSH PRIVATE KEY-----";
307
308    #[cfg(feature = "rsa")]
309    const RSA_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY-----
310b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
311NhAAAAAwEAAQAAAQEAuSvQ9m76zhRB4m0BUKPf17lwccj7KQ1Qtse63AOqP/VYItqEH8un
312rxPogXNBgrcCEm/ccLZZsyE3qgp3DRQkkqvJhZ6O8VBPsXxjZesRCqoFNCczy+Mf0R/Qmv
313Rnpu5+4DDLz0p7vrsRZW9ji/c98KzxeUonWgkplQaCBYLN875WdeUYMGtb1MLfNCEj177j
314gZl3CzttLRK3su6dckowXcXYv1gPTPZAwJb49J43o1QhV7+1zdwXvuFM6zuYHdu9ZHSKir
3156k1dXOET3/U+LWG5ofAo8oxUWv/7vs6h7MeajwkUeIBOWYtD+wGYRvVpxvj7nyOoWtg+jm
3160X6ndnsD+QAAA8irV+ZAq1fmQAAAAAdzc2gtcnNhAAABAQC5K9D2bvrOFEHibQFQo9/XuX
317BxyPspDVC2x7rcA6o/9Vgi2oQfy6evE+iBc0GCtwISb9xwtlmzITeqCncNFCSSq8mFno7x
318UE+xfGNl6xEKqgU0JzPL4x/RH9Ca9Gem7n7gMMvPSnu+uxFlb2OL9z3wrPF5SidaCSmVBo
319IFgs3zvlZ15Rgwa1vUwt80ISPXvuOBmXcLO20tErey7p1ySjBdxdi/WA9M9kDAlvj0njej
320VCFXv7XN3Be+4UzrO5gd271kdIqKvqTV1c4RPf9T4tYbmh8CjyjFRa//u+zqHsx5qPCRR4
321gE5Zi0P7AZhG9WnG+PufI6ha2D6ObRfqd2ewP5AAAAAwEAAQAAAQAdELqhI/RsSpO45eFR
3229hcZtnrm8WQzImrr9dfn1w9vMKSf++rHTuFIQvi48Q10ZiOGH1bbvlPAIVOqdjAPtnyzJR
323HhzmyjhjasJlk30zj+kod0kz63HzSMT9EfsYNfmYoCyMYFCKz52EU3xc87Vhi74XmZz0D0
324CgIj6TyZftmzC4YJCiwwU8K+29nxBhcbFRxpgwAksFL6PCSQsPl4y7yvXGcX+7lpZD8547
325v58q3jIkH1g2tBOusIuaiphDDStVJhVdKA55Z0Kju2kvCqsRIlf1efrq43blRgJFFFCxNZ
3268Cpolt4lOHhg+o3ucjILlCOgjDV8dB21YLxmgN5q+xFNAAAAgQC1P+eLUkHDFXnleCEVrW
327xL/DFxEyneLQz3IawGdw7cyAb7vxsYrGUvbVUFkxeiv397pDHLZ5U+t5cOYDBZ7G43Mt2g
328YfWBuRNvYhHA9Sdf38m5qPA6XCvm51f+FxInwd/kwRKH01RHJuRGsl/4Apu4DqVob8y00V
329WTYyV6JBNDkQAAAIEA322lj7ZJXfK/oLhMM/RS+DvaMea1g/q43mdRJFQQso4XRCL6IIVn
330oZXFeOxrMIRByVZBw+FSeB6OayWcZMySpJQBo70GdJOc3pJb3js0T+P2XA9+/jwXS58K9a
331+IkgLkv9XkfxNGNKyPEEzXC8QQzvjs1LbmO59VLko8ypwHq/cAAACBANQqaULI0qdwa0vm
332d3Ae1+k3YLZ0kapSQGVIMT2lkrhKV35tj7HIFpUPa4vitHzcUwtjYhqFezVF+JyPbJ/Fsp
333XmEc0g1fFnQp5/SkUwoN2zm8Up52GBelkq2Jk57mOMzWO0QzzNuNV/feJk02b2aE8rrAqP
334QR+u0AypRPmzHnOPAAAAEXJvb3RAMTQwOTExNTQ5NDBkAQ==
335-----END OPENSSH PRIVATE KEY-----";
336
337    #[test]
338    fn test_decode_ed25519_secret_key() {
339        env_logger::try_init().unwrap_or(());
340        decode_secret_key(ED25519_KEY, Some("blabla")).unwrap();
341    }
342
343    #[test]
344    fn test_decode_ed25519_aesctr_secret_key() {
345        env_logger::try_init().unwrap_or(());
346        decode_secret_key(ED25519_AESCTR_KEY, Some("test")).unwrap();
347    }
348
349    // Key from RFC 8410 Section 10.3. This is a key using PrivateKeyInfo structure.
350    const RFC8410_ED25519_PRIVATE_ONLY_KEY: &str = "-----BEGIN PRIVATE KEY-----
351MC4CAQAwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC
352-----END PRIVATE KEY-----";
353
354    #[test]
355    fn test_decode_rfc8410_ed25519_private_only_key() {
356        env_logger::try_init().unwrap_or(());
357        assert!(
358            decode_secret_key(RFC8410_ED25519_PRIVATE_ONLY_KEY, None)
359                .unwrap()
360                .algorithm()
361                == ssh_key::Algorithm::Ed25519,
362        );
363        // We always encode public key, skip test_decode_encode_symmetry.
364    }
365
366    // Key from RFC 8410 Section 10.3. This is a key using OneAsymmetricKey structure.
367    const RFC8410_ED25519_PRIVATE_PUBLIC_KEY: &str = "-----BEGIN PRIVATE KEY-----
368MHICAQEwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC
369oB8wHQYKKoZIhvcNAQkJFDEPDA1DdXJkbGUgQ2hhaXJzgSEAGb9ECWmEzf6FQbrB
370Z9w7lshQhqowtrbLDFw4rXAxZuE=
371-----END PRIVATE KEY-----";
372
373    #[test]
374    fn test_decode_rfc8410_ed25519_private_public_key() {
375        env_logger::try_init().unwrap_or(());
376        assert!(
377            decode_secret_key(RFC8410_ED25519_PRIVATE_PUBLIC_KEY, None)
378                .unwrap()
379                .algorithm()
380                == ssh_key::Algorithm::Ed25519,
381        );
382        // We can't encode attributes, skip test_decode_encode_symmetry.
383    }
384
385    #[cfg(feature = "rsa")]
386    #[test]
387    fn test_decode_rsa_secret_key() {
388        env_logger::try_init().unwrap_or(());
389        decode_secret_key(RSA_KEY, None).unwrap();
390    }
391
392    #[test]
393    fn test_decode_openssh_p256_secret_key() {
394        // Generated using: ssh-keygen -t ecdsa -b 256 -m rfc4716 -f $file
395        let key = "-----BEGIN OPENSSH PRIVATE KEY-----
396b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
3971zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQ/i+HCsmZZPy0JhtT64vW7EmeA1DeA
398M/VnPq3vAhu+xooJ7IMMK3lUHlBDosyvA2enNbCWyvNQc25dVt4oh9RhAAAAqHG7WMFxu1
399jBAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBD+L4cKyZlk/LQmG
4001Pri9bsSZ4DUN4Az9Wc+re8CG77GignsgwwreVQeUEOizK8DZ6c1sJbK81Bzbl1W3iiH1G
401EAAAAgLAmXR6IlN0SdiD6o8qr+vUr0mXLbajs/m0UlegElOmoAAAANcm9iZXJ0QGJic2Rl
402dgECAw==
403-----END OPENSSH PRIVATE KEY-----
404";
405        assert!(
406            decode_secret_key(key, None).unwrap().algorithm()
407                == ssh_key::Algorithm::Ecdsa {
408                    curve: ssh_key::EcdsaCurve::NistP256
409                },
410        );
411    }
412
413    #[test]
414    fn test_decode_openssh_p384_secret_key() {
415        // Generated using: ssh-keygen -t ecdsa -b 384 -m rfc4716 -f $file
416        let key = "-----BEGIN OPENSSH PRIVATE KEY-----
417b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
4181zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTkLnKPk/1NZD9mQ8XoebD7ASv9/svh
4195jO75HF7RYAqKK3fl5wsHe4VTJAOT3qH841yTcK79l0dwhHhHeg60byL7F9xOEzr2kqGeY
420Uwrl7fVaL7hfHzt6z+sG8smSQ3tF8AAADYHjjBch44wXIAAAATZWNkc2Etc2hhMi1uaXN0
421cDM4NAAAAAhuaXN0cDM4NAAAAGEE5C5yj5P9TWQ/ZkPF6Hmw+wEr/f7L4eYzu+Rxe0WAKi
422it35ecLB3uFUyQDk96h/ONck3Cu/ZdHcIR4R3oOtG8i+xfcThM69pKhnmFMK5e31Wi+4Xx
42387es/rBvLJkkN7RfAAAAMFzt6053dxaQT0Ta/CGfZna0nibHzxa55zgBmje/Ho3QDNlBCH
424Ylv0h4Wyzto8NfLQAAAA1yb2JlcnRAYmJzZGV2AQID
425-----END OPENSSH PRIVATE KEY-----
426";
427        assert!(
428            decode_secret_key(key, None).unwrap().algorithm()
429                == ssh_key::Algorithm::Ecdsa {
430                    curve: ssh_key::EcdsaCurve::NistP384
431                },
432        );
433    }
434
435    #[test]
436    fn test_decode_openssh_p521_secret_key() {
437        // Generated using: ssh-keygen -t ecdsa -b 521 -m rfc4716 -f $file
438        let key = "-----BEGIN OPENSSH PRIVATE KEY-----
439b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
4401zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQA7a9awmFeDjzYiuUOwMfXkKTevfQI
441iGlduu8BkjBOWXpffJpKsdTyJI/xI05l34OvqfCCkPUcfFWHK+LVRGahMBgBcGB9ZZOEEq
442iKNIT6C9WcJTGDqcBSzQ2yTSOxPXfUmVTr4D76vbYu5bjd9aBKx8HdfMvPeo0WD0ds/LjX
443LdJoDXcAAAEQ9fxlIfX8ZSEAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
444AAAIUEAO2vWsJhXg482IrlDsDH15Ck3r30CIhpXbrvAZIwTll6X3yaSrHU8iSP8SNOZd+D
445r6nwgpD1HHxVhyvi1URmoTAYAXBgfWWThBKoijSE+gvVnCUxg6nAUs0Nsk0jsT131JlU6+
446A++r22LuW43fWgSsfB3XzLz3qNFg9HbPy41y3SaA13AAAAQgH4DaftY0e/KsN695VJ06wy
447Ve0k2ddxoEsSE15H4lgNHM2iuYKzIqZJOReHRCTff6QGgMYPDqDfFfL1Hc1Ntql0pwAAAA
4481yb2JlcnRAYmJzZGV2AQIDBAU=
449-----END OPENSSH PRIVATE KEY-----
450";
451        assert!(
452            decode_secret_key(key, None).unwrap().algorithm()
453                == ssh_key::Algorithm::Ecdsa {
454                    curve: ssh_key::EcdsaCurve::NistP521
455                },
456        );
457    }
458
459    #[test]
460    fn test_fingerprint() {
461        let key = parse_public_key_base64(
462            "AAAAC3NzaC1lZDI1NTE5AAAAILagOJFgwaMNhBWQINinKOXmqS4Gh5NgxgriXwdOoINJ",
463        )
464        .unwrap();
465        assert_eq!(
466            format!("{}", key.fingerprint(ssh_key::HashAlg::Sha256)),
467            "SHA256:ldyiXa1JQakitNU5tErauu8DvWQ1dZ7aXu+rm7KQuog"
468        );
469    }
470
471    #[test]
472    fn test_parse_p256_public_key() {
473        env_logger::try_init().unwrap_or(());
474        let key = "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMxBTpMIGvo7CnordO7wP0QQRqpBwUjOLl4eMhfucfE1sjTYyK5wmTl1UqoSDS1PtRVTBdl+0+9pquFb46U7fwg=";
475
476        assert!(
477            parse_public_key_base64(key).unwrap().algorithm()
478                == ssh_key::Algorithm::Ecdsa {
479                    curve: ssh_key::EcdsaCurve::NistP256
480                },
481        );
482    }
483
484    #[test]
485    fn test_parse_p384_public_key() {
486        env_logger::try_init().unwrap_or(());
487        let key = "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBVFgxJxpCaAALZG/S5BHT8/IUQ5mfuKaj7Av9g7Jw59fBEGHfPBz1wFtHGYw5bdLmfVZTIDfogDid5zqJeAKr1AcD06DKTXDzd2EpUjqeLfQ5b3erHuX758fgu/pSDGRA==";
488
489        assert!(
490            parse_public_key_base64(key).unwrap().algorithm()
491                == ssh_key::Algorithm::Ecdsa {
492                    curve: ssh_key::EcdsaCurve::NistP384
493                }
494        );
495    }
496
497    #[test]
498    fn test_parse_p521_public_key() {
499        env_logger::try_init().unwrap_or(());
500        let key = "AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAAQepXEpOrzlX22r4E5zEHjhHWeZUe//zaevTanOWRBnnaCGWJFGCdjeAbNOuAmLtXc+HZdJTCZGREeSLSrpJa71QDCgZl0N7DkDUanCpHZJe/DCK6qwtHYbEMn28iLMlGCOrCIa060EyJHbp1xcJx4I1SKj/f/fm3DhhID/do6zyf8Cg==";
501
502        assert!(
503            parse_public_key_base64(key).unwrap().algorithm()
504                == ssh_key::Algorithm::Ecdsa {
505                    curve: ssh_key::EcdsaCurve::NistP521
506                }
507        );
508    }
509
510    #[test]
511    fn test_srhb() {
512        env_logger::try_init().unwrap_or(());
513        let key = "AAAAB3NzaC1yc2EAAAADAQABAAACAQC0Xtz3tSNgbUQAXem4d+d6hMx7S8Nwm/DOO2AWyWCru+n/+jQ7wz2b5+3oG2+7GbWZNGj8HCc6wJSA3jUsgv1N6PImIWclD14qvoqY3Dea1J0CJgXnnM1xKzBz9C6pDHGvdtySg+yzEO41Xt4u7HFn4Zx5SGuI2NBsF5mtMLZXSi33jCIWVIkrJVd7sZaY8jiqeVZBB/UvkLPWewGVuSXZHT84pNw4+S0Rh6P6zdNutK+JbeuO+5Bav4h9iw4t2sdRkEiWg/AdMoSKmo97Gigq2mKdW12ivnXxz3VfxrCgYJj9WwaUUWSfnAju5SiNly0cTEAN4dJ7yB0mfLKope1kRhPsNaOuUmMUqlu/hBDM/luOCzNjyVJ+0LLB7SV5vOiV7xkVd4KbEGKou8eeCR3yjFazUe/D1pjYPssPL8cJhTSuMc+/UC9zD8yeEZhB9V+vW4NMUR+lh5+XeOzenl65lWYd/nBZXLBbpUMf1AOfbz65xluwCxr2D2lj46iApSIpvE63i3LzFkbGl9GdUiuZJLMFJzOWdhGGc97cB5OVyf8umZLqMHjaImxHEHrnPh1MOVpv87HYJtSBEsN4/omINCMZrk++CRYAIRKRpPKFWV7NQHcvw3m7XLR3KaTYe+0/MINIZwGdou9fLUU3zSd521vDjA/weasH0CyDHq7sZw==";
514
515        parse_public_key_base64(key).unwrap();
516    }
517
518    #[cfg(feature = "rsa")]
519    #[test]
520    fn test_nikao() {
521        env_logger::try_init().unwrap_or(());
522        let key = "-----BEGIN RSA PRIVATE KEY-----
523MIIEpQIBAAKCAQEAw/FG8YLVoXhsUVZcWaY7iZekMxQ2TAfSVh0LTnRuzsumeLhb
5240fh4scIt4C4MLwpGe/u3vj290C28jLkOtysqnIpB4iBUrFNRmEz2YuvjOzkFE8Ju
5250l1VrTZ9APhpLZvzT2N7YmTXcLz1yWopCe4KqTHczEP4lfkothxEoACXMaxezt5o
526wIYfagDaaH6jXJgJk1SQ5VYrROVpDjjX8/Zg01H1faFQUikYx0M8EwL1fY5B80Hd
5276DYSok8kUZGfkZT8HQ54DBgocjSs449CVqkVoQC1aDB+LZpMWovY15q7hFgfQmYD
528qulbZRWDxxogS6ui/zUR2IpX7wpQMKKkBS1qdQIDAQABAoIBAQCodpcCKfS2gSzP
529uapowY1KvP/FkskkEU18EDiaWWyzi1AzVn5LRo+udT6wEacUAoebLU5K2BaMF+aW
530Lr1CKnDWaeA/JIDoMDJk+TaU0i5pyppc5LwXTXvOEpzi6rCzL/O++88nR4AbQ7sm
531Uom6KdksotwtGvttJe0ktaUi058qaoFZbels5Fwk5bM5GHDdV6De8uQjSfYV813P
532tM/6A5rRVBjC5uY0ocBHxPXkqAdHfJuVk0uApjLrbm6k0M2dg1X5oyhDOf7ZIzAg
533QGPgvtsVZkQlyrD1OoCMPwzgULPXTe8SktaP9EGvKdMf5kQOqUstqfyx+E4OZa0A
534T82weLjBAoGBAOUChhaLQShL3Vsml/Nuhhw5LsxU7Li34QWM6P5AH0HMtsSncH8X
535ULYcUKGbCmmMkVb7GtsrHa4ozy0fjq0Iq9cgufolytlvC0t1vKRsOY6poC2MQgaZ
536bqRa05IKwhZdHTr9SUwB/ngtVNWRzzbFKLkn2W5oCpQGStAKqz3LbKstAoGBANsJ
537EyrXPbWbG+QWzerCIi6shQl+vzOd3cxqWyWJVaZglCXtlyySV2eKWRW7TcVvaXQr
538Nzm/99GNnux3pUCY6szy+9eevjFLLHbd+knzCZWKTZiWZWr503h/ztfFwrMzhoAh
539z4nukD/OETugPvtG01c2sxZb/F8LH9KORznhlSlpAoGBAJnqg1J9j3JU4tZTbwcG
540fo5ThHeCkINp2owPc70GPbvMqf4sBzjz46QyDaM//9SGzFwocplhNhaKiQvrzMnR
541LSVucnCEm/xdXLr/y6S6tEiFCwnx3aJv1uQRw2bBYkcDmBTAjVXPdUcyOHU+BYXr
542Jv6ioMlKlel8/SUsNoFWypeVAoGAXhr3Bjf1xlm+0O9PRyZjQ0RR4DN5eHbB/XpQ
543cL8hclsaK3V5tuek79JL1f9kOYhVeVi74G7uzTSYbCY3dJp+ftGCjDAirNEMaIGU
544cEMgAgSqs/0h06VESwg2WRQZQ57GkbR1E2DQzuj9FG4TwSe700OoC9o3gqon4PHJ
545/j9CM8kCgYEAtPJf3xaeqtbiVVzpPAGcuPyajTzU0QHPrXEl8zr/+iSK4Thc1K+c
546b9sblB+ssEUQD5IQkhTWcsXdslINQeL77WhIMZ2vBAH8Hcin4jgcLmwUZfpfnnFs
547QaChXiDsryJZwsRnruvMRX9nedtqHrgnIsJLTXjppIhGhq5Kg4RQfOU=
548-----END RSA PRIVATE KEY-----
549";
550        decode_secret_key(key, None).unwrap();
551    }
552
553    #[cfg(feature = "rsa")]
554    #[test]
555    fn test_decode_pkcs8_rsa_secret_key() {
556        // Generated using: ssh-keygen -t rsa -b 1024 -m pkcs8 -f $file
557        let key = "-----BEGIN PRIVATE KEY-----
558MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDTwWfiCKHw/1F6
559pvm6hZpFSjCVSu4Pp0/M4xT9Cec1+2uj/6uEE9Vh/UhlerkxVbrW/YaqjnlAiemZ
5600RGN+sq7b8LxsgvOAo7gdBv13TLkKxNFiRbSy8S257uA9/K7G4Uw+NW22zoLSKCp
561pdJOFzaYMIT/UX9EOq9hIIn4bS4nXJ4V5+aHBtMddHHDQPEDHBHuifpP2L4Wopzu
562WoQoVtN9cwHSLh0Bd7uT+X9useIJrFzcsxVXwD2WGfR59Ue3rxRu6JqC46Klf55R
5635NQ8OQ+7NHXjW5HO076W1GXcnhGKT5CGjglTdk5XxQkNZsz72cHu7RDaADdWAWnE
564hSyH7flrAgMBAAECggEAbFdpCjn2eTJ4grOJ1AflTYxO3SOQN8wXxTFuHKUDehgg
565E7GNFK99HnyTnPA0bmx5guQGEZ+BpCarsXpJbAYj0dC1wimhZo7igS6G272H+zua
566yZoBZmrBQ/++bJbvxxGmjM7TsZHq2bkYEpR3zGKOGUHB2kvdPJB2CNC4JrXdxl7q
567djjsr5f/SreDmHqcNBe1LcyWLSsuKTfwTKhsE1qEe6QA2uOpUuFrsdPoeYrfgapu
568sK6qnpxvOTJHCN/9jjetrP2fGl78FMBYfXzjAyKSKzLvzOwMAmcHxy50RgUvezx7
569A1RwMpB7VoV0MOpcAjlQ1T7YDH9avdPMzp0EZ24y+QKBgQD/MxDJjHu33w13MnIg
570R4BrgXvrgL89Zde5tML2/U9C2LRvFjbBvgnYdqLsuqxDxGY/8XerrAkubi7Fx7QI
571m2uvTOZF915UT/64T35zk8nAAFhzicCosVCnBEySvdwaaBKoj/ywemGrwoyprgFe
572r8LGSo42uJi0zNf5IxmVzrDlRwKBgQDUa3P/+GxgpUYnmlt63/7sII6HDssdTHa9
573x5uPy8/2ackNR7FruEAJR1jz6akvKnvtbCBeRxLeOFwsseFta8rb2vks7a/3I8ph
574gJlbw5Bttpc+QsNgC61TdSKVsfWWae+YT77cfGPM4RaLlxRnccW1/HZjP2AMiDYG
575WCiluO+svQKBgQC3a/yk4FQL1EXZZmigysOCgY6Ptfm+J3TmBQYcf/R4F0mYjl7M
5764coxyxNPEty92Gulieh5ey0eMhNsFB1SEmNTm/HmV+V0tApgbsJ0T8SyO41Xfar7
577lHZjlLN0xQFt+V9vyA3Wyh9pVGvFiUtywuE7pFqS+hrH2HNindfF1MlQAQKBgQDF
578YxBIxKzY5duaA2qMdMcq3lnzEIEXua0BTxGz/n1CCizkZUFtyqnetWjoRrGK/Zxp
579FDfDw6G50397nNPQXQEFaaZv5HLGYYC3N8vKJKD6AljqZxmsD03BprA7kEGYwtn8
580m+XMdt46TNMpZXt1YJiLMo1ETmjPXGdvX85tqLs2tQKBgQDCbwd+OBzSiic3IQlD
581E/OHAXH6HNHmUL3VD5IiRh4At2VAIl8JsmafUvvbtr5dfT3PA8HB6sDG4iXQsBbR
582oTSAo/DtIWt1SllGx6MvcPqL1hp1UWfoIGTnE3unHtgPId+DnjMbTcuZOuGl7evf
583abw8VeY2goORjpBXsfydBETbgQ==
584-----END PRIVATE KEY-----
585";
586        assert!(decode_secret_key(key, None).unwrap().algorithm().is_rsa());
587        test_decode_encode_symmetry(key);
588    }
589
590    #[test]
591    fn test_decode_pkcs8_p256_secret_key() {
592        // Generated using: ssh-keygen -t ecdsa -b 256 -m pkcs8 -f $file
593        let key = "-----BEGIN PRIVATE KEY-----
594MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE0C7/pyJDcZTAgWo
595ydj6EE8QkZ91jtGoGmdYAVd7LaqhRANCAATWkGOof7R/PAUuOr2+ZPUgB8rGVvgr
596qa92U3p4fkJToKXku5eq/32OBj23YMtz76jO3yfMbtG3l1JWLowPA8tV
597-----END PRIVATE KEY-----
598";
599        assert!(
600            decode_secret_key(key, None).unwrap().algorithm()
601                == ssh_key::Algorithm::Ecdsa {
602                    curve: ssh_key::EcdsaCurve::NistP256
603                },
604        );
605        test_decode_encode_symmetry(key);
606    }
607
608    #[test]
609    fn test_decode_pkcs8_p384_secret_key() {
610        // Generated using: ssh-keygen -t ecdsa -b 384 -m pkcs8 -f $file
611        let key = "-----BEGIN PRIVATE KEY-----
612MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCaqAL30kg+T5BUOYG9
613MrzeDXiUwy9LM8qJGNXiMYou0pVjFZPZT3jAsrUQo47PLQ6hZANiAARuEHbXJBYK
6149uyJj4PjT56OHjT2GqMa6i+FTG9vdLtu4OLUkXku+kOuFNjKvEI1JYBrJTpw9kSZ
615CI3WfCsQvVjoC7m8qRyxuvR3Rv8gGXR1coQciIoCurLnn9zOFvXCS2Y=
616-----END PRIVATE KEY-----
617";
618        assert!(
619            decode_secret_key(key, None).unwrap().algorithm()
620                == ssh_key::Algorithm::Ecdsa {
621                    curve: ssh_key::EcdsaCurve::NistP384
622                },
623        );
624        test_decode_encode_symmetry(key);
625    }
626
627    #[test]
628    fn test_decode_pkcs8_p521_secret_key() {
629        // Generated using: ssh-keygen -t ecdsa -b 521 -m pkcs8 -f $file
630        let key = "-----BEGIN PRIVATE KEY-----
631MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIB1As9UBUsCiMK7Rzs
632EoMgqDM/TK7y7+HgCWzw5UujXvSXCzYCeBgfJszn7dVoJE9G/1ejmpnVTnypdKEu
633iIvd4LyhgYkDgYYABAADBCrg7hkomJbCsPMuMcq68ulmo/6Tv8BDS13F8T14v5RN
634/0iT/+nwp6CnbBFewMI2TOh/UZNyPpQ8wOFNn9zBmAFCMzkQibnSWK0hrRstY5LT
635iaOYDwInbFDsHu8j3TGs29KxyVXMexeV6ROQyXzjVC/quT1R5cOQ7EadE4HvaWhT
636Ow==
637-----END PRIVATE KEY-----
638";
639        assert!(
640            decode_secret_key(key, None).unwrap().algorithm()
641                == ssh_key::Algorithm::Ecdsa {
642                    curve: ssh_key::EcdsaCurve::NistP521
643                },
644        );
645        test_decode_encode_symmetry(key);
646    }
647
648    #[test]
649    #[cfg(feature = "legacy-ed25519-pkcs8-parser")]
650    fn test_decode_pkcs8_ed25519_generated_by_russh_0_43() -> Result<(), crate::keys::Error> {
651        // Generated by russh 0.43
652        let key = "-----BEGIN PRIVATE KEY-----
653MHMCAQEwBQYDK2VwBEIEQBHw4cXPpGgA+KdvPF5gxrzML+oa3yQk0JzIbWvmqM5H30RyBF8GrOWz
654p77UAd3O4PgYzzFcUc79g8yKtbKhzJGhIwMhAN9EcgRfBqzls6e+1AHdzuD4GM8xXFHO/YPMirWy
655ocyR
656
657-----END PRIVATE KEY-----
658";
659
660        assert!(decode_secret_key(key, None)?.algorithm() == ssh_key::Algorithm::Ed25519,);
661
662        let k = decode_secret_key(key, None)?;
663        let inner = k.key_data().ed25519().unwrap();
664
665        assert_eq!(
666            &inner.private.to_bytes(),
667            &[
668                17, 240, 225, 197, 207, 164, 104, 0, 248, 167, 111, 60, 94, 96, 198, 188, 204, 47,
669                234, 26, 223, 36, 36, 208, 156, 200, 109, 107, 230, 168, 206, 71
670            ]
671        );
672
673        Ok(())
674    }
675
676    fn test_decode_encode_symmetry(key: &str) {
677        let original_key_bytes = data_encoding::BASE64_MIME
678            .decode(
679                key.lines()
680                    .filter(|line| !line.starts_with("-----"))
681                    .collect::<Vec<&str>>()
682                    .join("")
683                    .as_bytes(),
684            )
685            .unwrap();
686        let decoded_key = decode_secret_key(key, None).unwrap();
687        let encoded_key_bytes = pkcs8::encode_pkcs8(&decoded_key).unwrap();
688        assert_eq!(original_key_bytes, encoded_key_bytes);
689    }
690
691    #[cfg(feature = "rsa")]
692    #[test]
693    fn test_o01eg() {
694        env_logger::try_init().unwrap_or(());
695
696        let key = "-----BEGIN RSA PRIVATE KEY-----
697Proc-Type: 4,ENCRYPTED
698DEK-Info: AES-128-CBC,EA77308AAF46981303D8C44D548D097E
699
700QR18hXmAgGehm1QMMYGF34PAtBpTj+8/ZPFx2zZxir7pzDpfYoNAIf/fzLsW1ruG
7010xo/ZK/T3/TpMgjmLsCR6q+KU4jmCcCqWQIGWYJt9ljFI5y/CXr5uqP3DKcqtdxQ
702fbBAfXJ8ITF+Tj0Cljm2S1KYHor+mkil5Lf/ZNiHxcLfoI3xRnpd+2cemN9Ly9eY
703HNTbeWbLosfjwdfPJNWFNV5flm/j49klx/UhXhr5HNFNgp/MlTrvkH4rBt4wYPpE
704cZBykt4Fo1KGl95pT22inGxQEXVHF1Cfzrf5doYWxjiRTmfhpPSz/Tt0ev3+jIb8
705Htx6N8tNBoVxwCiQb7jj3XNim2OGohIp5vgW9sh6RDfIvr1jphVOgCTFKSo37xk0
706156EoCVo3VcLf+p0/QitbUHR+RGW/PvUJV/wFR5ShYqjI+N2iPhkD24kftJ/MjPt
707AAwCm/GYoYjGDhIzQMB+FETZKU5kz23MQtZFbYjzkcI/RE87c4fkToekNCdQrsoZ
708wG0Ne2CxrwwEnipHCqT4qY+lZB9EbqQgbWOXJgxA7lfznBFjdSX7uDc/mnIt9Y6B
709MZRXH3PTfotHlHMe+Ypt5lfPBi/nruOl5wLo3L4kY5pUyqR0cXKNycIJZb/pJAnE
710ryIb59pZP7njvoHzRqnC9dycnTFW3geK5LU+4+JMUS32F636aorunRCl6IBmVQHL
711uZ+ue714fn/Sn6H4dw6IH1HMDG1hr8ozP4sNUCiAQ05LsjDMGTdrUsr2iBBpkQhu
712VhUDZy9g/5XF1EgiMbZahmqi5WaJ5K75ToINHb7RjOE7MEiuZ+RPpmYLE0HXyn9X
713HTx0ZGr022dDI6nkvUm6OvEwLUUmmGKRHKe0y1EdICGNV+HWqnlhGDbLWeMyUcIY
714M6Zh9Dw3WXD3kROf5MrJ6n9MDIXx9jy7nmBh7m6zKjBVIw94TE0dsRcWb0O1IoqS
715zLQ6ihno+KsQHDyMVLEUz1TuE52rIpBmqexDm3PdDfCgsNdBKP6QSTcoqcfHKeex
716K93FWgSlvFFQQAkJumJJ+B7ZWnK+2pdjdtWwTpflAKNqc8t//WmjWZzCtbhTHCXV
7171dnMk7azWltBAuXnjW+OqmuAzyh3ayKgqfW66mzSuyQNa1KqFhqpJxOG7IHvxVfQ
718kYeSpqODnL87Zd/dU8s0lOxz3/ymtjPMHlOZ/nHNqW90IIeUwWJKJ46Kv6zXqM1t
719MeD1lvysBbU9rmcUdop0D3MOgGpKkinR5gy4pUsARBiz4WhIm8muZFIObWes/GDS
720zmmkQRO1IcfXKAHbq/OdwbLBm4vM9nk8vPfszoEQCnfOSd7aWrLRjDR+q2RnzNzh
721K+fodaJ864JFIfB/A+aVviVWvBSt0eEbEawhTmNPerMrAQ8tRRhmNxqlDP4gOczi
722iKUmK5recsXk5us5Ik7peIR/f9GAghpoJkF0HrHio47SfABuK30pzcj62uNWGljS
7233d9UQLCepT6RiPFhks/lgimbtSoiJHql1H9Q/3q4MuO2PuG7FXzlTnui3zGw/Vvy
724br8gXU8KyiY9sZVbmplRPF+ar462zcI2kt0a18mr0vbrdqp2eMjb37QDbVBJ+rPE
725-----END RSA PRIVATE KEY-----
726";
727        decode_secret_key(key, Some("12345")).unwrap();
728    }
729
730    #[cfg(feature = "rsa")]
731    pub const PKCS8_RSA: &str = "-----BEGIN RSA PRIVATE KEY-----
732MIIEpAIBAAKCAQEAwBGetHjW+3bDQpVktdemnk7JXgu1NBWUM+ysifYLDBvJ9ttX
733GNZSyQKA4v/dNr0FhAJ8I9BuOTjYCy1YfKylhl5D/DiSSXFPsQzERMmGgAlYvU2U
734+FTxpBC11EZg69CPVMKKevfoUD+PZA5zB7Hc1dXFfwqFc5249SdbAwD39VTbrOUI
735WECvWZs6/ucQxHHXP2O9qxWqhzb/ddOnqsDHUNoeceiNiCf2anNymovrIMjAqq1R
736t2UP3f06/Zt7Jx5AxKqS4seFkaDlMAK8JkEDuMDOdKI36raHkKanfx8CnGMSNjFQ
737QtvnpD8VSGkDTJN3Qs14vj2wvS477BQXkBKN1QIDAQABAoIBABb6xLMw9f+2ENyJ
738hTggagXsxTjkS7TElCu2OFp1PpMfTAWl7oDBO7xi+UqvdCcVbHCD35hlWpqsC2Ui
7398sBP46n040ts9UumK/Ox5FWaiuYMuDpF6vnfJ94KRcb0+KmeFVf9wpW9zWS0hhJh
740jC+yfwpyfiOZ/ad8imGCaOguGHyYiiwbRf381T/1FlaOGSae88h+O8SKTG1Oahq4
7410HZ/KBQf9pij0mfVQhYBzsNu2JsHNx9+DwJkrXT7K9SHBpiBAKisTTCnQmS89GtE
7426J2+bq96WgugiM7X6OPnmBmE/q1TgV18OhT+rlvvNi5/n8Z1ag5Xlg1Rtq/bxByP
743CeIVHsECgYEA9dX+LQdv/Mg/VGIos2LbpJUhJDj0XWnTRq9Kk2tVzr+9aL5VikEb
74409UPIEa2ToL6LjlkDOnyqIMd/WY1W0+9Zf1ttg43S/6Rvv1W8YQde0Nc7QTcuZ1K
7459jSSP9hzsa3KZtx0fCtvVHm+ac9fP6u80tqumbiD2F0cnCZcSxOb4+UCgYEAyAKJ
74670nNKegH4rTCStAqR7WGAsdPE3hBsC814jguplCpb4TwID+U78Xxu0DQF8WtVJ10
747SJuR0R2q4L9uYWpo0MxdawSK5s9Am27MtJL0mkFQX0QiM7hSZ3oqimsdUdXwxCGg
748oktxCUUHDIPJNVd4Xjg0JTh4UZT6WK9hl1zLQzECgYEAiZRCFGc2KCzVLF9m0cXA
749kGIZUxFAyMqBv+w3+zq1oegyk1z5uE7pyOpS9cg9HME2TAo4UPXYpLAEZ5z8vWZp
75045sp/BoGnlQQsudK8gzzBtnTNp5i/MnnetQ/CNYVIVnWjSxRUHBqdMdRZhv0/Uga
751e5KA5myZ9MtfSJA7VJTbyHUCgYBCcS13M1IXaMAt3JRqm+pftfqVs7YeJqXTrGs/
752AiDlGQigRk4quFR2rpAV/3rhWsawxDmb4So4iJ16Wb2GWP4G1sz1vyWRdSnmOJGC
753LwtYrvfPHegqvEGLpHa7UsgDpol77hvZriwXwzmLO8A8mxkeW5dfAfpeR5o+mcxW
754pvnTEQKBgQCKx6Ln0ku6jDyuDzA9xV2/PET5D75X61R2yhdxi8zurY/5Qon3OWzk
755jn/nHT3AZghGngOnzyv9wPMKt9BTHyTB6DlB6bRVLDkmNqZh5Wi8U1/IjyNYI0t2
756xV/JrzLAwPoKk3bkqys3bUmgo6DxVC/6RmMwPQ0rmpw78kOgEej90g==
757-----END RSA PRIVATE KEY-----
758";
759
760    #[cfg(feature = "rsa")]
761    #[test]
762    fn test_pkcs8() {
763        env_logger::try_init().unwrap_or(());
764        println!("test");
765        decode_secret_key(PKCS8_RSA, Some("blabla")).unwrap();
766    }
767
768    #[cfg(feature = "rsa")]
769    const PKCS8_ENCRYPTED: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----
770MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQITo1O0b8YrS0CAggA
771MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBtLH4T1KOfo1GGr7salhR8BIIE
7720KN9ednYwcTGSX3hg7fROhTw7JAJ1D4IdT1fsoGeNu2BFuIgF3cthGHe6S5zceI2
773MpkfwvHbsOlDFWMUIAb/VY8/iYxhNmd5J6NStMYRC9NC0fVzOmrJqE1wITqxtORx
774IkzqkgFUbaaiFFQPepsh5CvQfAgGEWV329SsTOKIgyTj97RxfZIKA+TR5J5g2dJY
775j346SvHhSxJ4Jc0asccgMb0HGh9UUDzDSql0OIdbnZW5KzYJPOx+aDqnpbz7UzY/
776P8N0w/pEiGmkdkNyvGsdttcjFpOWlLnLDhtLx8dDwi/sbEYHtpMzsYC9jPn3hnds
777TcotqjoSZ31O6rJD4z18FOQb4iZs3MohwEdDd9XKblTfYKM62aQJWH6cVQcg+1C7
778jX9l2wmyK26Tkkl5Qg/qSfzrCveke5muZgZkFwL0GCcgPJ8RixSB4GOdSMa/hAMU
779kvFAtoV2GluIgmSe1pG5cNMhurxM1dPPf4WnD+9hkFFSsMkTAuxDZIdDk3FA8zof
780Yhv0ZTfvT6V+vgH3Hv7Tqcxomy5Qr3tj5vvAqqDU6k7fC4FvkxDh2mG5ovWvc4Nb
781Xv8sed0LGpYitIOMldu6650LoZAqJVv5N4cAA2Edqldf7S2Iz1QnA/usXkQd4tLa
782Z80+sDNv9eCVkfaJ6kOVLk/ghLdXWJYRLenfQZtVUXrPkaPpNXgD0dlaTN8KuvML
783Uw/UGa+4ybnPsdVflI0YkJKbxouhp4iB4S5ACAwqHVmsH5GRnujf10qLoS7RjDAl
784o/wSHxdT9BECp7TT8ID65u2mlJvH13iJbktPczGXt07nBiBse6OxsClfBtHkRLzE
785QF6UMEXsJnIIMRfrZQnduC8FUOkfPOSXc8r9SeZ3GhfbV/DmWZvFPCpjzKYPsM5+
786N8Bw/iZ7NIH4xzNOgwdp5BzjH9hRtCt4sUKVVlWfEDtTnkHNOusQGKu7HkBF87YZ
787RN/Nd3gvHob668JOcGchcOzcsqsgzhGMD8+G9T9oZkFCYtwUXQU2XjMN0R4VtQgZ
788rAxWyQau9xXMGyDC67gQ5xSn+oqMK0HmoW8jh2LG/cUowHFAkUxdzGadnjGhMOI2
789zwNJPIjF93eDF/+zW5E1l0iGdiYyHkJbWSvcCuvTwma9FIDB45vOh5mSR+YjjSM5
790nq3THSWNi7Cxqz12Q1+i9pz92T2myYKBBtu1WDh+2KOn5DUkfEadY5SsIu/Rb7ub
7915FBihk2RN3y/iZk+36I69HgGg1OElYjps3D+A9AjVby10zxxLAz8U28YqJZm4wA/
792T0HLxBiVw+rsHmLP79KvsT2+b4Diqih+VTXouPWC/W+lELYKSlqnJCat77IxgM9e
793YIhzD47OgWl33GJ/R10+RDoDvY4koYE+V5NLglEhbwjloo9Ryv5ywBJNS7mfXMsK
794/uf+l2AscZTZ1mhtL38efTQCIRjyFHc3V31DI0UdETADi+/Omz+bXu0D5VvX+7c6
795b1iVZKpJw8KUjzeUV8yOZhvGu3LrQbhkTPVYL555iP1KN0Eya88ra+FUKMwLgjYr
796JkUx4iad4dTsGPodwEP/Y9oX/Qk3ZQr+REZ8lg6IBoKKqqrQeBJ9gkm1jfKE6Xkc
797Cog3JMeTrb3LiPHgN6gU2P30MRp6L1j1J/MtlOAr5rux
798-----END ENCRYPTED PRIVATE KEY-----";
799
800    #[test]
801    fn test_gpg() {
802        env_logger::try_init().unwrap_or(());
803        let key = [
804            0, 0, 0, 7, 115, 115, 104, 45, 114, 115, 97, 0, 0, 0, 3, 1, 0, 1, 0, 0, 1, 129, 0, 163,
805            72, 59, 242, 4, 248, 139, 217, 57, 126, 18, 195, 170, 3, 94, 154, 9, 150, 89, 171, 236,
806            192, 178, 185, 149, 73, 210, 121, 95, 126, 225, 209, 199, 208, 89, 130, 175, 229, 163,
807            102, 176, 155, 69, 199, 155, 71, 214, 170, 61, 202, 2, 207, 66, 198, 147, 65, 10, 176,
808            20, 105, 197, 133, 101, 126, 193, 252, 245, 254, 182, 14, 250, 118, 113, 18, 220, 38,
809            220, 75, 247, 50, 163, 39, 2, 61, 62, 28, 79, 199, 238, 189, 33, 194, 190, 22, 87, 91,
810            1, 215, 115, 99, 138, 124, 197, 127, 237, 228, 170, 42, 25, 117, 1, 106, 36, 54, 163,
811            163, 207, 129, 133, 133, 28, 185, 170, 217, 12, 37, 113, 181, 182, 180, 178, 23, 198,
812            233, 31, 214, 226, 114, 146, 74, 205, 177, 82, 232, 238, 165, 44, 5, 250, 150, 236, 45,
813            30, 189, 254, 118, 55, 154, 21, 20, 184, 235, 223, 5, 20, 132, 249, 147, 179, 88, 146,
814            6, 100, 229, 200, 221, 157, 135, 203, 57, 204, 43, 27, 58, 85, 54, 219, 138, 18, 37,
815            80, 106, 182, 95, 124, 140, 90, 29, 48, 193, 112, 19, 53, 84, 201, 153, 52, 249, 15,
816            41, 5, 11, 147, 18, 8, 27, 31, 114, 45, 224, 118, 111, 176, 86, 88, 23, 150, 184, 252,
817            128, 52, 228, 90, 30, 34, 135, 234, 123, 28, 239, 90, 202, 239, 188, 175, 8, 141, 80,
818            59, 194, 80, 43, 205, 34, 137, 45, 140, 244, 181, 182, 229, 247, 94, 216, 115, 173,
819            107, 184, 170, 102, 78, 249, 4, 186, 234, 169, 148, 98, 128, 33, 115, 232, 126, 84, 76,
820            222, 145, 90, 58, 1, 4, 163, 243, 93, 215, 154, 205, 152, 178, 109, 241, 197, 82, 148,
821            222, 78, 44, 193, 248, 212, 157, 118, 217, 75, 211, 23, 229, 121, 28, 180, 208, 173,
822            204, 14, 111, 226, 25, 163, 220, 95, 78, 175, 189, 168, 67, 159, 179, 176, 200, 150,
823            202, 248, 174, 109, 25, 89, 176, 220, 226, 208, 187, 84, 169, 157, 14, 88, 217, 221,
824            117, 254, 51, 45, 93, 184, 80, 225, 158, 29, 76, 38, 69, 72, 71, 76, 50, 191, 210, 95,
825            152, 175, 26, 207, 91, 7,
826        ];
827        ssh_key::PublicKey::decode(&key).unwrap();
828    }
829
830    #[cfg(feature = "rsa")]
831    #[test]
832    fn test_pkcs8_encrypted() {
833        env_logger::try_init().unwrap_or(());
834        println!("test");
835        decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap();
836    }
837
838    #[cfg(unix)]
839    async fn test_client_agent(key: PrivateKey) -> Result<(), Box<dyn std::error::Error>> {
840        env_logger::try_init().unwrap_or(());
841        use std::process::Stdio;
842
843        let dir = tempfile::tempdir()?;
844        let agent_path = dir.path().join("agent");
845        let mut agent = tokio::process::Command::new("ssh-agent")
846            .arg("-a")
847            .arg(&agent_path)
848            .arg("-D")
849            .stdout(Stdio::null())
850            .stderr(Stdio::null())
851            .spawn()?;
852
853        // Wait for the socket to be created
854        while agent_path.canonicalize().is_err() {
855            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
856        }
857
858        let public = key.public_key();
859        let stream = tokio::net::UnixStream::connect(&agent_path).await?;
860        let mut client = agent::client::AgentClient::connect(stream);
861        client.add_identity(&key, &[]).await?;
862        client.request_identities().await?;
863        let buf = b"blabla".to_vec();
864        let len = buf.len();
865        let buf = client
866            .sign_request(
867                &AgentIdentity::from(public.clone()),
868                Some(HashAlg::Sha256),
869                buf,
870            )
871            .await
872            .unwrap();
873        let (a, b) = buf.split_at(len);
874
875        match key.public_key().key_data() {
876            ssh_key::public::KeyData::Ed25519 { .. } => {
877                let sig = &b[b.len() - 64..];
878                let sig = ssh_key::Signature::new(key.algorithm(), sig)?;
879                use signature::Verifier;
880                assert!(Verifier::verify(public, a, &sig).is_ok());
881            }
882            ssh_key::public::KeyData::Ecdsa { .. } => {}
883            _ => {}
884        }
885
886        agent.kill().await?;
887        agent.wait().await?;
888
889        Ok(())
890    }
891
892    #[tokio::test]
893    #[cfg(unix)]
894    async fn test_client_agent_ed25519() {
895        let key = decode_secret_key(ED25519_KEY, Some("blabla")).unwrap();
896        test_client_agent(key).await.expect("ssh-agent test failed")
897    }
898
899    #[tokio::test]
900    #[cfg(all(unix, feature = "rsa"))]
901    async fn test_client_agent_rsa() {
902        let key = decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap();
903        test_client_agent(key).await.expect("ssh-agent test failed")
904    }
905
906    #[tokio::test]
907    #[cfg(all(unix, feature = "rsa"))]
908    async fn test_client_agent_openssh_rsa() {
909        let key = decode_secret_key(RSA_KEY, None).unwrap();
910        test_client_agent(key).await.expect("ssh-agent test failed")
911    }
912
913    #[test]
914    #[cfg(all(unix, feature = "rsa"))]
915    fn test_agent() {
916        env_logger::try_init().unwrap_or(());
917        let dir = tempfile::tempdir().unwrap();
918        let agent_path = dir.path().join("agent");
919
920        let core = tokio::runtime::Runtime::new().unwrap();
921        use agent;
922        use signature::Verifier;
923
924        #[derive(Clone)]
925        struct X {}
926        impl agent::server::Agent for X {
927            fn confirm(
928                self,
929                _: std::sync::Arc<PrivateKey>,
930            ) -> Box<dyn Future<Output = (Self, bool)> + Send + Unpin> {
931                Box::new(futures::future::ready((self, true)))
932            }
933        }
934        let agent_path_ = agent_path.clone();
935        let (tx, rx) = tokio::sync::oneshot::channel();
936        core.spawn(async move {
937            let mut listener = tokio::net::UnixListener::bind(&agent_path_).unwrap();
938            let _ = tx.send(());
939            agent::server::serve(
940                Incoming {
941                    listener: &mut listener,
942                },
943                X {},
944            )
945            .await
946        });
947
948        let key = decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap();
949        core.block_on(async move {
950            let public = key.public_key();
951            // make sure the listener created the file handle
952            rx.await.unwrap();
953            let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap();
954            let mut client = agent::client::AgentClient::connect(stream);
955            client
956                .add_identity(&key, &[agent::Constraint::KeyLifetime { seconds: 60 }])
957                .await
958                .unwrap();
959            client.request_identities().await.unwrap();
960            let buf = b"blabla".to_vec();
961            let len = buf.len();
962            let buf = client
963                .sign_request(&AgentIdentity::from(public.clone()), None, buf)
964                .await
965                .unwrap();
966            let (a, b) = buf.split_at(len);
967            if let ssh_key::public::KeyData::Ed25519 { .. } = public.key_data() {
968                let sig = &b[b.len() - 64..];
969                let sig = ssh_key::Signature::new(key.algorithm(), sig).unwrap();
970                assert!(Verifier::verify(public, a, &sig).is_ok());
971            }
972        })
973    }
974
975    #[cfg(unix)]
976    struct Incoming<'a> {
977        listener: &'a mut tokio::net::UnixListener,
978    }
979
980    #[cfg(unix)]
981    impl futures::stream::Stream for Incoming<'_> {
982        type Item = Result<tokio::net::UnixStream, std::io::Error>;
983
984        fn poll_next(
985            self: std::pin::Pin<&mut Self>,
986            cx: &mut std::task::Context<'_>,
987        ) -> std::task::Poll<Option<Self::Item>> {
988            let (sock, _addr) = futures::ready!(self.get_mut().listener.poll_accept(cx))?;
989            std::task::Poll::Ready(Some(Ok(sock)))
990        }
991    }
992
993    /// Helper to spawn an ssh-agent and return the agent process and socket path
994    #[cfg(unix)]
995    async fn spawn_agent() -> Result<
996        (tokio::process::Child, std::path::PathBuf, tempfile::TempDir),
997        Box<dyn std::error::Error>,
998    > {
999        use std::process::Stdio;
1000
1001        let dir = tempfile::tempdir()?;
1002        let agent_path = dir.path().join("agent");
1003        let agent = tokio::process::Command::new("ssh-agent")
1004            .arg("-a")
1005            .arg(&agent_path)
1006            .arg("-D")
1007            .stdout(Stdio::null())
1008            .stderr(Stdio::null())
1009            .spawn()?;
1010
1011        // Wait for the socket to be created
1012        while agent_path.canonicalize().is_err() {
1013            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1014        }
1015
1016        Ok((agent, agent_path, dir))
1017    }
1018
1019    /// Helper to create a test certificate
1020    #[cfg(unix)]
1021    fn create_test_cert(ca_key: &PrivateKey, user_key: &PrivateKey) -> ssh_key::Certificate {
1022        use ssh_key::certificate;
1023                use std::time::{SystemTime, UNIX_EPOCH};
1024
1025        let now = SystemTime::now()
1026            .duration_since(UNIX_EPOCH)
1027            .unwrap()
1028            .as_secs();
1029        let valid_after = now - 3600; // 1 hour ago
1030        let valid_before = now + 86400 * 365; // 1 year from now
1031
1032        let mut builder = certificate::Builder::new_with_random_nonce(
1033            &mut rand::rng(),
1034            user_key.public_key(),
1035            valid_after,
1036            valid_before,
1037        )
1038        .unwrap();
1039
1040        builder.serial(1).unwrap();
1041        builder.key_id("test-cert").unwrap();
1042        builder.cert_type(certificate::CertType::User).unwrap();
1043        builder.valid_principal("testuser").unwrap();
1044        builder.sign(ca_key).unwrap()
1045    }
1046
1047    #[tokio::test]
1048    #[cfg(unix)]
1049    async fn test_request_identities_full_with_keys_and_certs() {
1050        use crate::keys::agent::{AgentIdentity, client::AgentClient};
1051                use std::io::Write;
1052        use std::process::Stdio;
1053
1054        env_logger::try_init().unwrap_or(());
1055
1056        let (mut agent, agent_path, dir) = spawn_agent().await.unwrap();
1057
1058        // Create a CA key and user key
1059        let ca_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap();
1060        let user_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap();
1061        let plain_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap();
1062
1063        // Create a certificate
1064        let cert = create_test_cert(&ca_key, &user_key);
1065
1066        // Write the keys and certificate to temp files
1067        let user_key_path = dir.path().join("user_key");
1068        let cert_path = dir.path().join("user_key-cert.pub");
1069        let plain_key_path = dir.path().join("plain_key");
1070
1071        // Write user key (the one to be certified)
1072        let mut f = std::fs::File::create(&user_key_path).unwrap();
1073        f.write_all(
1074            user_key
1075                .to_openssh(ssh_key::LineEnding::LF)
1076                .unwrap()
1077                .as_bytes(),
1078        )
1079        .unwrap();
1080        std::fs::set_permissions(
1081            &user_key_path,
1082            std::os::unix::fs::PermissionsExt::from_mode(0o600),
1083        )
1084        .unwrap();
1085
1086        // Write certificate
1087        let mut f = std::fs::File::create(&cert_path).unwrap();
1088        f.write_all(cert.to_openssh().unwrap().as_bytes()).unwrap();
1089
1090        // Write plain key
1091        let mut f = std::fs::File::create(&plain_key_path).unwrap();
1092        f.write_all(
1093            plain_key
1094                .to_openssh(ssh_key::LineEnding::LF)
1095                .unwrap()
1096                .as_bytes(),
1097        )
1098        .unwrap();
1099        std::fs::set_permissions(
1100            &plain_key_path,
1101            std::os::unix::fs::PermissionsExt::from_mode(0o600),
1102        )
1103        .unwrap();
1104
1105        // Use ssh-add to add the certificate (it will pick up user_key-cert.pub automatically)
1106        let status = tokio::process::Command::new("ssh-add")
1107            .arg(&user_key_path)
1108            .env("SSH_AUTH_SOCK", &agent_path)
1109            .stdout(Stdio::null())
1110            .stderr(Stdio::null())
1111            .status()
1112            .await
1113            .unwrap();
1114        assert!(status.success(), "ssh-add for certificate failed");
1115
1116        // Add plain key
1117        let status = tokio::process::Command::new("ssh-add")
1118            .arg(&plain_key_path)
1119            .env("SSH_AUTH_SOCK", &agent_path)
1120            .stdout(Stdio::null())
1121            .stderr(Stdio::null())
1122            .status()
1123            .await
1124            .unwrap();
1125        assert!(status.success(), "ssh-add for plain key failed");
1126
1127        // Connect to agent and test request_identities_full
1128        let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap();
1129        let mut client = AgentClient::connect(stream);
1130
1131        let identities = client.request_identities().await.unwrap();
1132
1133        // ssh-add with a certificate adds the cert identity, and the plain key adds another
1134        // The exact count depends on ssh-agent behavior - just verify we have both types
1135        assert!(!identities.is_empty(), "Expected at least one identity");
1136
1137        // Count the types
1138        let mut key_count = 0;
1139        let mut cert_count = 0;
1140
1141        for identity in &identities {
1142            match identity {
1143                AgentIdentity::PublicKey { .. } => key_count += 1,
1144                AgentIdentity::Certificate { certificate: c, .. } => {
1145                    cert_count += 1;
1146                    // Verify the certificate matches what we created
1147                    assert_eq!(c.key_id(), "test-cert");
1148                    // Verify public_key() works
1149                    let pk = identity.public_key();
1150                    assert_eq!(pk.key_data(), c.public_key());
1151                }
1152            }
1153            // Verify comment() works
1154            let _ = identity.comment();
1155        }
1156
1157        // We should have at least one of each (ssh-add may add both key and cert for the same identity)
1158        assert!(
1159            key_count >= 1,
1160            "Expected at least 1 public key, got {}",
1161            key_count
1162        );
1163        assert!(
1164            cert_count >= 1,
1165            "Expected at least 1 certificate, got {}",
1166            cert_count
1167        );
1168
1169        agent.kill().await.unwrap();
1170        agent.wait().await.unwrap();
1171    }
1172
1173    #[tokio::test]
1174    #[cfg(unix)]
1175    async fn test_sign_request_cert() {
1176        use crate::keys::agent::client::AgentClient;
1177                use std::io::Write;
1178        use std::process::Stdio;
1179
1180        env_logger::try_init().unwrap_or(());
1181
1182        let (mut agent, agent_path, dir) = spawn_agent().await.unwrap();
1183
1184        // Create a CA key and user key
1185        let ca_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap();
1186        let user_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap();
1187
1188        // Create a certificate
1189        let cert = create_test_cert(&ca_key, &user_key);
1190
1191        // Write the key and certificate to temp files
1192        let user_key_path = dir.path().join("user_key");
1193        let cert_path = dir.path().join("user_key-cert.pub");
1194
1195        let mut f = std::fs::File::create(&user_key_path).unwrap();
1196        f.write_all(
1197            user_key
1198                .to_openssh(ssh_key::LineEnding::LF)
1199                .unwrap()
1200                .as_bytes(),
1201        )
1202        .unwrap();
1203        std::fs::set_permissions(
1204            &user_key_path,
1205            std::os::unix::fs::PermissionsExt::from_mode(0o600),
1206        )
1207        .unwrap();
1208
1209        let mut f = std::fs::File::create(&cert_path).unwrap();
1210        f.write_all(cert.to_openssh().unwrap().as_bytes()).unwrap();
1211
1212        // Use ssh-add to add the certificate
1213        let status = tokio::process::Command::new("ssh-add")
1214            .arg(&user_key_path)
1215            .env("SSH_AUTH_SOCK", &agent_path)
1216            .stdout(Stdio::null())
1217            .stderr(Stdio::null())
1218            .status()
1219            .await
1220            .unwrap();
1221        assert!(status.success(), "ssh-add failed");
1222
1223        // Connect to agent and test sign_request_cert
1224        let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap();
1225        let mut client = AgentClient::connect(stream);
1226
1227        // Create data to sign
1228        let data_to_sign = b"test data to sign";
1229        let buf = data_to_sign.to_vec();
1230        let len = buf.len();
1231
1232        // Sign using the certificate (None for hash_alg since Ed25519 doesn't need it)
1233        let signed_buf = client.sign_request(&cert.into(), None, buf).await.unwrap();
1234
1235        // Verify the signature is appended to the original data
1236        assert!(signed_buf.len() > len, "Signed buffer should be larger");
1237
1238        // Extract and verify signature
1239        let (original, sig_data) = signed_buf.split_at(len);
1240        assert_eq!(original, data_to_sign);
1241
1242        // The signature should be valid
1243        // For ed25519, signature is 64 bytes, but encoded with type prefix
1244        assert!(sig_data.len() > 64, "Signature data should include type");
1245
1246        agent.kill().await.unwrap();
1247        agent.wait().await.unwrap();
1248    }
1249
1250    #[tokio::test]
1251    #[cfg(unix)]
1252    async fn test_sign_request_cert_missing_key_returns_agent_failure() {
1253        use crate::keys::agent::client::AgentClient;
1254        
1255        env_logger::try_init().unwrap_or(());
1256
1257        let (mut agent, agent_path, _dir) = spawn_agent().await.unwrap();
1258
1259        // Create a CA key and user key, but DON'T add them to the agent
1260        let ca_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap();
1261        let user_key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap();
1262
1263        // Create a certificate
1264        let cert = create_test_cert(&ca_key, &user_key);
1265
1266        // Connect to agent WITHOUT adding any keys
1267        let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap();
1268        let mut client = AgentClient::connect(stream);
1269
1270        // Verify the agent has no keys
1271        let identities = client.request_identities().await.unwrap();
1272        assert!(identities.is_empty(), "Agent should have no keys");
1273
1274        // Create data to sign
1275        let data_to_sign = b"test data to sign";
1276        let buf = data_to_sign.to_vec();
1277
1278        // Try to sign using the certificate - should fail because the key isn't in the agent
1279        let result = client.sign_request(&cert.into(), None, buf).await;
1280
1281        // Verify we get an AgentFailure error
1282        assert!(
1283            result.is_err(),
1284            "Signing should fail when key is not in agent"
1285        );
1286        match result {
1287            Err(Error::AgentFailure) => {
1288                // This is the expected error
1289            }
1290            Err(e) => {
1291                panic!("Expected AgentFailure error, got: {:?}", e);
1292            }
1293            Ok(_) => {
1294                panic!("Expected error, but signing succeeded");
1295            }
1296        }
1297
1298        agent.kill().await.unwrap();
1299        agent.wait().await.unwrap();
1300    }
1301
1302    #[tokio::test]
1303    #[cfg(unix)]
1304    async fn test_sign_request_missing_key_returns_agent_failure() {
1305        use crate::keys::agent::client::AgentClient;
1306        
1307        env_logger::try_init().unwrap_or(());
1308
1309        let (mut agent, agent_path, _dir) = spawn_agent().await.unwrap();
1310
1311        // Create a key but DON'T add it to the agent
1312        let key = PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519).unwrap();
1313
1314        // Connect to agent WITHOUT adding any keys
1315        let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap();
1316        let mut client = AgentClient::connect(stream);
1317
1318        // Verify the agent has no keys
1319        let identities = client.request_identities().await.unwrap();
1320        assert!(identities.is_empty(), "Agent should have no keys");
1321
1322        // Create data to sign
1323        let data_to_sign = b"test data to sign";
1324        let buf = data_to_sign.to_vec();
1325
1326        // Try to sign using the public key - should fail because the key isn't in the agent
1327        let result = client
1328            .sign_request(&key.public_key().clone().into(), None, buf)
1329            .await;
1330
1331        // Verify we get an AgentFailure error
1332        assert!(
1333            result.is_err(),
1334            "Signing should fail when key is not in agent"
1335        );
1336        match result {
1337            Err(Error::AgentFailure) => {
1338                // This is the expected error
1339            }
1340            Err(e) => {
1341                panic!("Expected AgentFailure error, got: {:?}", e);
1342            }
1343            Ok(_) => {
1344                panic!("Expected error, but signing succeeded");
1345            }
1346        }
1347
1348        agent.kill().await.unwrap();
1349        agent.wait().await.unwrap();
1350    }
1351
1352    /// Helper to create a test RSA certificate
1353    #[cfg(all(unix, feature = "rsa"))]
1354    fn create_test_rsa_cert(ca_key: &PrivateKey, user_key: &PrivateKey) -> ssh_key::Certificate {
1355        use ssh_key::certificate;
1356                use std::time::{SystemTime, UNIX_EPOCH};
1357
1358        let now = SystemTime::now()
1359            .duration_since(UNIX_EPOCH)
1360            .unwrap()
1361            .as_secs();
1362        let valid_after = now - 3600; // 1 hour ago
1363        let valid_before = now + 86400 * 365; // 1 year from now
1364
1365        let mut builder = certificate::Builder::new_with_random_nonce(
1366            &mut rand::rng(),
1367            user_key.public_key(),
1368            valid_after,
1369            valid_before,
1370        )
1371        .unwrap();
1372
1373        builder.serial(1).unwrap();
1374        builder.key_id("test-rsa-cert").unwrap();
1375        builder.cert_type(certificate::CertType::User).unwrap();
1376        builder.valid_principal("testuser").unwrap();
1377        builder.sign(ca_key).unwrap()
1378    }
1379
1380    #[tokio::test]
1381    #[cfg(all(unix, feature = "rsa"))]
1382    async fn test_sign_request_cert_rsa() {
1383        use crate::keys::agent::client::AgentClient;
1384                use std::io::Write;
1385        use std::process::Stdio;
1386
1387        env_logger::try_init().unwrap_or(());
1388
1389        let (mut agent, agent_path, dir) = spawn_agent().await.unwrap();
1390
1391        // Create RSA CA key and user key
1392        let ca_key = PrivateKey::random(
1393            &mut rand::rng(),
1394            ssh_key::Algorithm::Rsa {
1395                hash: Some(HashAlg::Sha256),
1396            },
1397        )
1398        .unwrap();
1399        let user_key = PrivateKey::random(
1400            &mut rand::rng(),
1401            ssh_key::Algorithm::Rsa {
1402                hash: Some(HashAlg::Sha256),
1403            },
1404        )
1405        .unwrap();
1406
1407        // Create a certificate
1408        let cert = create_test_rsa_cert(&ca_key, &user_key);
1409
1410        // Write the key and certificate to temp files
1411        let user_key_path = dir.path().join("user_rsa_key");
1412        let cert_path = dir.path().join("user_rsa_key-cert.pub");
1413
1414        let mut f = std::fs::File::create(&user_key_path).unwrap();
1415        f.write_all(
1416            user_key
1417                .to_openssh(ssh_key::LineEnding::LF)
1418                .unwrap()
1419                .as_bytes(),
1420        )
1421        .unwrap();
1422        std::fs::set_permissions(
1423            &user_key_path,
1424            std::os::unix::fs::PermissionsExt::from_mode(0o600),
1425        )
1426        .unwrap();
1427
1428        let mut f = std::fs::File::create(&cert_path).unwrap();
1429        f.write_all(cert.to_openssh().unwrap().as_bytes()).unwrap();
1430
1431        // Use ssh-add to add the certificate
1432        let status = tokio::process::Command::new("ssh-add")
1433            .arg(&user_key_path)
1434            .env("SSH_AUTH_SOCK", &agent_path)
1435            .stdout(Stdio::null())
1436            .stderr(Stdio::null())
1437            .status()
1438            .await
1439            .unwrap();
1440        assert!(status.success(), "ssh-add failed");
1441
1442        // Connect to agent and test sign_request_cert
1443        let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap();
1444        let mut client = AgentClient::connect(stream);
1445
1446        // Create data to sign
1447        let data_to_sign = b"test data to sign with RSA cert";
1448        let buf = data_to_sign.to_vec();
1449        let len = buf.len();
1450
1451        // Sign using the certificate with SHA-256 hash algorithm
1452        let signed_buf = client
1453            .sign_request(&cert.into(), Some(HashAlg::Sha256), buf)
1454            .await
1455            .unwrap();
1456
1457        // Verify the signature is appended to the original data
1458        assert!(signed_buf.len() > len, "Signed buffer should be larger");
1459
1460        // Extract and verify signature
1461        let (original, sig_data) = signed_buf.split_at(len);
1462        assert_eq!(original, data_to_sign);
1463
1464        // The RSA signature should be substantial
1465        assert!(
1466            sig_data.len() > 100,
1467            "RSA signature data should be substantial"
1468        );
1469
1470        agent.kill().await.unwrap();
1471        agent.wait().await.unwrap();
1472    }
1473
1474    #[tokio::test]
1475    #[cfg(all(unix, feature = "rsa"))]
1476    async fn test_sign_request_cert_rsa_sha512() {
1477        use crate::keys::agent::client::AgentClient;
1478                use std::io::Write;
1479        use std::process::Stdio;
1480
1481        env_logger::try_init().unwrap_or(());
1482
1483        let (mut agent, agent_path, dir) = spawn_agent().await.unwrap();
1484
1485        // Create RSA CA key and user key
1486        let ca_key = PrivateKey::random(
1487            &mut rand::rng(),
1488            ssh_key::Algorithm::Rsa {
1489                hash: Some(HashAlg::Sha512),
1490            },
1491        )
1492        .unwrap();
1493        let user_key = PrivateKey::random(
1494            &mut rand::rng(),
1495            ssh_key::Algorithm::Rsa {
1496                hash: Some(HashAlg::Sha512),
1497            },
1498        )
1499        .unwrap();
1500
1501        // Create a certificate
1502        let cert = create_test_rsa_cert(&ca_key, &user_key);
1503
1504        // Write the key and certificate to temp files
1505        let user_key_path = dir.path().join("user_rsa_key");
1506        let cert_path = dir.path().join("user_rsa_key-cert.pub");
1507
1508        let mut f = std::fs::File::create(&user_key_path).unwrap();
1509        f.write_all(
1510            user_key
1511                .to_openssh(ssh_key::LineEnding::LF)
1512                .unwrap()
1513                .as_bytes(),
1514        )
1515        .unwrap();
1516        std::fs::set_permissions(
1517            &user_key_path,
1518            std::os::unix::fs::PermissionsExt::from_mode(0o600),
1519        )
1520        .unwrap();
1521
1522        let mut f = std::fs::File::create(&cert_path).unwrap();
1523        f.write_all(cert.to_openssh().unwrap().as_bytes()).unwrap();
1524
1525        // Use ssh-add to add the certificate
1526        let status = tokio::process::Command::new("ssh-add")
1527            .arg(&user_key_path)
1528            .env("SSH_AUTH_SOCK", &agent_path)
1529            .stdout(Stdio::null())
1530            .stderr(Stdio::null())
1531            .status()
1532            .await
1533            .unwrap();
1534        assert!(status.success(), "ssh-add failed");
1535
1536        // Connect to agent and test sign_request_cert with SHA-512
1537        let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap();
1538        let mut client = AgentClient::connect(stream);
1539
1540        // Create data to sign
1541        let data_to_sign = b"test data to sign with RSA cert SHA-512";
1542        let buf = data_to_sign.to_vec();
1543        let len = buf.len();
1544
1545        // Sign using the certificate with SHA-512 hash algorithm
1546        let signed_buf = client
1547            .sign_request(&cert.into(), Some(HashAlg::Sha512), buf)
1548            .await
1549            .unwrap();
1550
1551        // Verify the signature is appended to the original data
1552        assert!(signed_buf.len() > len, "Signed buffer should be larger");
1553
1554        // Extract and verify signature
1555        let (original, sig_data) = signed_buf.split_at(len);
1556        assert_eq!(original, data_to_sign);
1557
1558        // The RSA signature should be substantial
1559        assert!(
1560            sig_data.len() > 100,
1561            "RSA signature data should be substantial"
1562        );
1563
1564        agent.kill().await.unwrap();
1565        agent.wait().await.unwrap();
1566    }
1567}