Skip to main content

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