Skip to main content

auths_core/agent/
session.rs

1//! SSH agent session handler.
2//!
3//! This module provides `AgentSession`, which implements the `ssh_agent_lib::agent::Session`
4//! trait to handle SSH agent protocol requests.
5
6use crate::agent::AgentHandle;
7use crate::error::AgentError as AuthsAgentError;
8use log::{debug, error, warn};
9#[cfg(unix)]
10use ssh_agent_lib::agent::Agent;
11use ssh_agent_lib::agent::Session;
12use ssh_agent_lib::error::AgentError as SSHAgentError;
13use ssh_agent_lib::proto::{AddIdentity, Credential, Identity, RemoveIdentity, SignRequest};
14use ssh_key::private::KeypairData;
15use ssh_key::public::{Ed25519PublicKey, KeyData};
16use ssh_key::{Algorithm, Signature};
17use std::convert::TryInto;
18use std::io;
19use std::sync::Arc;
20use zeroize::Zeroizing;
21
22/// The identity of a process connected to the agent socket, read from the peer
23/// credentials of the connection.
24///
25/// Args (fields): `uid`, `pid`.
26///
27/// Usage:
28/// ```ignore
29/// let peer = PeerIdentity { uid: 1000, pid: Some(4242) };
30/// ```
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct PeerIdentity {
33    /// Effective user id of the connecting process.
34    pub uid: u32,
35    /// Process id of the connecting process, when the platform exposes it (Linux via
36    /// `SO_PEERCRED`; `None` on macOS, whose `getpeereid` returns no pid).
37    pub pid: Option<i32>,
38}
39
40impl PeerIdentity {
41    /// The agent's own process — used for in-process / default sessions where no
42    /// external peer applies.
43    pub fn local() -> Self {
44        Self { uid: 0, pid: None }
45    }
46}
47
48/// Decides whether a connected peer may make the agent sign *right now*.
49///
50/// Connection-level UID authorization (same user only) is enforced separately. This is
51/// the per-request gate that lets the host require approval — e.g. a per-caller
52/// biometric — before each signature, so an unlocked agent does not grant silent
53/// signing to every same-user process.
54pub trait SignAuthorizer: Send + Sync {
55    /// Returns true iff `peer` is allowed to obtain a signature now.
56    ///
57    /// Args:
58    /// * `peer`: the connecting process's identity.
59    fn authorize_sign(&self, peer: &PeerIdentity) -> bool;
60}
61
62/// The permissive authorizer: every signature is allowed. Used for in-process/default
63/// sessions and for explicitly non-interactive (headless/automation) contexts.
64///
65/// Usage:
66/// ```ignore
67/// let auth = std::sync::Arc::new(AllowAllSigning);
68/// ```
69pub struct AllowAllSigning;
70
71impl SignAuthorizer for AllowAllSigning {
72    fn authorize_sign(&self, _peer: &PeerIdentity) -> bool {
73        true
74    }
75}
76
77/// Per-caller signing approval — the policy for #354. The first time a given peer
78/// process asks to sign, the injected `approve` function is consulted (e.g. a
79/// biometric / user prompt). Approved peers are pinned for the life of this authorizer
80/// (the unlock window), so the legitimate caller is not re-prompted on every signature,
81/// while a *different* process triggers a fresh approval.
82///
83/// Peers are keyed by `(uid, pid)`. On platforms without a peer pid (macOS), `pid` is
84/// `None`, so callers collapse to per-uid; there the host's `approve` function should
85/// apply a time-bucketed re-auth rather than pinning forever.
86///
87/// Usage:
88/// ```ignore
89/// let auth = PerCallerAuthorizer::new(|peer| prompt_biometric_for(peer));
90/// ```
91pub struct PerCallerAuthorizer {
92    approve: Box<dyn Fn(&PeerIdentity) -> bool + Send + Sync>,
93    approved: std::sync::Mutex<std::collections::HashSet<(u32, Option<i32>)>>,
94}
95
96impl PerCallerAuthorizer {
97    /// Builds a per-caller authorizer that consults `approve` once per new peer and
98    /// pins peers it approves.
99    ///
100    /// Args:
101    /// * `approve`: called for a not-yet-approved peer; returning true pins it.
102    pub fn new(approve: impl Fn(&PeerIdentity) -> bool + Send + Sync + 'static) -> Self {
103        Self {
104            approve: Box::new(approve),
105            approved: std::sync::Mutex::new(std::collections::HashSet::new()),
106        }
107    }
108}
109
110impl SignAuthorizer for PerCallerAuthorizer {
111    fn authorize_sign(&self, peer: &PeerIdentity) -> bool {
112        let key = (peer.uid, peer.pid);
113        let mut approved = self
114            .approved
115            .lock()
116            .unwrap_or_else(|poisoned| poisoned.into_inner());
117        if approved.contains(&key) {
118            return true;
119        }
120        if (self.approve)(peer) {
121            approved.insert(key);
122            true
123        } else {
124            false
125        }
126    }
127}
128
129/// Wraps an `AgentHandle` to implement the `ssh_agent_lib::agent::Session` trait.
130///
131/// Each `AgentSession` holds a reference to an `AgentHandle`, the identity of the peer
132/// it serves, and the per-request `SignAuthorizer` consulted before each signature.
133#[derive(Clone)]
134pub struct AgentSession {
135    /// Reference to the agent handle
136    handle: Arc<AgentHandle>,
137    /// The connecting peer this session serves.
138    peer: PeerIdentity,
139    /// Per-request gate consulted before every signature.
140    authorizer: Arc<dyn SignAuthorizer>,
141}
142
143impl AgentSession {
144    /// Creates a session that allows every signature (in-process / default use).
145    ///
146    /// Args:
147    /// * `handle`: the agent handle holding the unlocked keys.
148    pub fn new(handle: Arc<AgentHandle>) -> Self {
149        Self {
150            handle,
151            peer: PeerIdentity::local(),
152            authorizer: Arc::new(AllowAllSigning),
153        }
154    }
155
156    /// Creates a session for a specific peer, gated by `authorizer` on every sign.
157    ///
158    /// Args:
159    /// * `handle`: the agent handle holding the unlocked keys.
160    /// * `peer`: the connecting process's identity.
161    /// * `authorizer`: the per-request signing gate.
162    pub fn with_authorizer(
163        handle: Arc<AgentHandle>,
164        peer: PeerIdentity,
165        authorizer: Arc<dyn SignAuthorizer>,
166    ) -> Self {
167        Self {
168            handle,
169            peer,
170            authorizer,
171        }
172    }
173
174    /// Returns a reference to the underlying agent handle.
175    pub fn handle(&self) -> &AgentHandle {
176        &self.handle
177    }
178}
179
180impl std::fmt::Debug for AgentSession {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        f.debug_struct("AgentSession")
183            .field("socket_path", self.handle.socket_path())
184            .field("is_running", &self.handle.is_running())
185            .finish()
186    }
187}
188
189/// Build a PKCS#8 v2 DER encoding for an Ed25519 key from seed and public key.
190///
191/// Ring's `Ed25519KeyPair::from_pkcs8()` requires v2 format (RFC 8410) which
192/// includes the public key. Produces the fixed 85-byte structure:
193pub use auths_crypto::build_ed25519_pkcs8_v2;
194
195#[ssh_agent_lib::async_trait]
196impl Session for AgentSession {
197    async fn request_identities(&mut self) -> Result<Vec<Identity>, SSHAgentError> {
198        let core = self.handle.lock().map_err(|_| {
199            error!("AgentSession failed to lock agent core (mutex poisoned).");
200            SSHAgentError::Failure
201        })?;
202
203        let pubkey_byte_vectors = core.public_keys();
204        debug!(
205            "request_identities: Agent core has {} keys.",
206            pubkey_byte_vectors.len()
207        );
208
209        let identities = pubkey_byte_vectors
210            .into_iter()
211            .filter_map(|pubkey_bytes| {
212                let key_data = match pubkey_bytes.len() {
213                    32 => {
214                        let key_bytes_array: &[u8; 32] = pubkey_bytes.as_slice().try_into().ok()?;
215                        KeyData::Ed25519(Ed25519PublicKey(*key_bytes_array))
216                    }
217                    33 | 65 => {
218                        // P-256: try to parse as ECDSA SEC1 point
219                        let ecdsa_pk =
220                            ssh_key::public::EcdsaPublicKey::from_sec1_bytes(&pubkey_bytes).ok()?;
221                        KeyData::Ecdsa(ecdsa_pk)
222                    }
223                    n => {
224                        warn!("request_identities: unsupported key length ({n}). Skipping.");
225                        return None;
226                    }
227                };
228                let comment = format!(
229                    "auths-key-{}",
230                    hex::encode(&pubkey_bytes[..4.min(pubkey_bytes.len())])
231                );
232                debug!("Adding identity with comment: {}", comment);
233                Some(Identity {
234                    pubkey: key_data,
235                    comment,
236                })
237            })
238            .collect();
239
240        Ok(identities)
241    }
242
243    async fn sign(&mut self, request: SignRequest) -> Result<Signature, SSHAgentError> {
244        if !self.authorizer.authorize_sign(&self.peer) {
245            warn!(
246                "Sign request refused: peer not authorized to sign (uid={}, pid={:?})",
247                self.peer.uid, self.peer.pid
248            );
249            return Err(SSHAgentError::Failure);
250        }
251
252        debug!(
253            "Handling sign request for key type: {:?}",
254            request.pubkey.algorithm()
255        );
256
257        let (pubkey_bytes_to_sign_with, algorithm) = match &request.pubkey {
258            KeyData::Ed25519(key) => (key.as_ref().to_vec(), Algorithm::Ed25519),
259            KeyData::Ecdsa(ecdsa_key) => {
260                let point_bytes = ecdsa_key.as_ref();
261                (
262                    point_bytes.to_vec(),
263                    Algorithm::Ecdsa {
264                        curve: ssh_key::EcdsaCurve::NistP256,
265                    },
266                )
267            }
268            other_key_type => {
269                let err_msg = format!(
270                    "Unsupported key type requested for signing: {:?}",
271                    other_key_type.algorithm()
272                );
273                error!("{}", err_msg);
274                return Err(SSHAgentError::other(io::Error::new(
275                    io::ErrorKind::Unsupported,
276                    err_msg,
277                )));
278            }
279        };
280
281        match self.handle.sign(&pubkey_bytes_to_sign_with, &request.data) {
282            Ok(signature_bytes) => {
283                debug!("Successfully signed data using agent core.");
284                Signature::new(algorithm, signature_bytes).map_err(|e| {
285                    let err_msg = format!(
286                        "Internal error: Failed to create ssh_key::Signature from core signature: {}",
287                        e
288                    );
289                    error!("{}", err_msg);
290                    SSHAgentError::other(io::Error::new(io::ErrorKind::InvalidData, err_msg))
291                })
292            }
293            Err(AuthsAgentError::KeyNotFound) => {
294                warn!("Sign request failed: Key not found in agent core.");
295                Err(SSHAgentError::Failure)
296            }
297            Err(AuthsAgentError::AgentLocked) => {
298                warn!("Sign request refused: agent is locked.");
299                Err(SSHAgentError::Failure)
300            }
301            Err(other_core_error) => {
302                let err_msg = format!("Agent core signing error: {}", other_core_error);
303                error!("{}", err_msg);
304                Err(SSHAgentError::other(io::Error::other(err_msg)))
305            }
306        }
307    }
308
309    async fn add_identity(&mut self, identity: AddIdentity) -> Result<(), SSHAgentError> {
310        debug!("Handling add_identity request");
311
312        let pkcs8_bytes: Zeroizing<Vec<u8>> = match &identity.credential {
313            Credential::Key { privkey, .. } => match privkey {
314                KeypairData::Ed25519(kp) => {
315                    let seed = kp.private.to_bytes();
316                    let pubkey = kp.public.0;
317                    build_ed25519_pkcs8_v2(&seed, &pubkey)
318                }
319                KeypairData::Ecdsa(ssh_key::private::EcdsaKeypair::NistP256 {
320                    private, ..
321                }) => {
322                    use auths_crypto::{TypedSeed, TypedSignerKey};
323                    let scalar_bytes = private.as_slice();
324                    if scalar_bytes.len() != 32 {
325                        let err_msg = format!(
326                            "Invalid P-256 scalar length: expected 32, got {}",
327                            scalar_bytes.len()
328                        );
329                        error!("{}", err_msg);
330                        return Err(SSHAgentError::other(io::Error::new(
331                            io::ErrorKind::InvalidData,
332                            err_msg,
333                        )));
334                    }
335                    #[allow(clippy::expect_used)] // INVARIANT: length checked above
336                    let mut scalar = [0u8; 32];
337                    scalar.copy_from_slice(scalar_bytes);
338                    let signer =
339                        TypedSignerKey::from_seed(TypedSeed::P256(scalar)).map_err(|e| {
340                            let err_msg = format!("P-256 signer construction failed: {}", e);
341                            error!("{}", err_msg);
342                            SSHAgentError::other(io::Error::other(err_msg))
343                        })?;
344                    let pkcs8 = signer.to_pkcs8().map_err(|e| {
345                        let err_msg = format!("P-256 PKCS8 encoding failed: {}", e);
346                        error!("{}", err_msg);
347                        SSHAgentError::other(io::Error::other(err_msg))
348                    })?;
349                    Zeroizing::new(pkcs8.as_ref().to_vec())
350                }
351                other => {
352                    let err_msg = format!(
353                        "Unsupported key type for add_identity: {:?}",
354                        other.algorithm()
355                    );
356                    error!("{}", err_msg);
357                    return Err(SSHAgentError::other(io::Error::new(
358                        io::ErrorKind::Unsupported,
359                        err_msg,
360                    )));
361                }
362            },
363            Credential::Cert { .. } => {
364                error!("Certificate credentials are not supported for add_identity");
365                return Err(SSHAgentError::other(io::Error::new(
366                    io::ErrorKind::Unsupported,
367                    "Certificate credentials are not supported",
368                )));
369            }
370        };
371
372        self.handle.register_key(pkcs8_bytes).map_err(|e| {
373            let err_msg = format!("Failed to register key in agent: {}", e);
374            error!("{}", err_msg);
375            SSHAgentError::other(io::Error::other(err_msg))
376        })?;
377
378        debug!("Successfully added identity to agent");
379        Ok(())
380    }
381
382    async fn remove_identity(&mut self, identity: RemoveIdentity) -> Result<(), SSHAgentError> {
383        debug!("Handling remove_identity request");
384
385        let pubkey_bytes = match &identity.pubkey {
386            KeyData::Ed25519(key) => key.as_ref().to_vec(),
387            KeyData::Ecdsa(key) => key.as_ref().to_vec(),
388            other => {
389                let err_msg = format!(
390                    "Unsupported key type for remove_identity: {:?}",
391                    other.algorithm()
392                );
393                error!("{}", err_msg);
394                return Err(SSHAgentError::other(io::Error::new(
395                    io::ErrorKind::Unsupported,
396                    err_msg,
397                )));
398            }
399        };
400
401        let mut core = self.handle.lock().map_err(|_| {
402            error!("AgentSession failed to lock agent core (mutex poisoned).");
403            SSHAgentError::Failure
404        })?;
405
406        core.unregister_key(&pubkey_bytes).map_err(|e| {
407            let err_msg = format!("Failed to remove key from agent: {}", e);
408            error!("{}", err_msg);
409            SSHAgentError::other(io::Error::new(io::ErrorKind::NotFound, err_msg))
410        })?;
411
412        debug!("Successfully removed identity from agent");
413        Ok(())
414    }
415
416    async fn remove_all_identities(&mut self) -> Result<(), SSHAgentError> {
417        debug!("Handling remove_all_identities request");
418
419        let mut core = self.handle.lock().map_err(|_| {
420            error!("AgentSession failed to lock agent core (mutex poisoned).");
421            SSHAgentError::Failure
422        })?;
423
424        core.clear_keys();
425        debug!("Successfully removed all identities from agent");
426        Ok(())
427    }
428}
429
430/// Returns whether a connecting peer is allowed to use the agent.
431///
432/// Only the user that owns the running agent may request signatures; a connection
433/// from any other user is refused.
434///
435/// Args:
436/// * `peer_uid`: The effective user id of the connecting process.
437/// * `owner_uid`: The effective user id that owns the agent.
438///
439/// Usage:
440/// ```ignore
441/// if peer_is_authorized(peer_uid, owner_uid) { /* serve the connection */ }
442/// ```
443fn peer_is_authorized(peer_uid: u32, owner_uid: u32) -> bool {
444    peer_uid == owner_uid
445}
446
447/// A session for a connection, gated on peer authorization.
448///
449/// `Authorized` connections are served by an `AgentSession`; `Denied` connections
450/// have every request refused, so an unauthorized peer can neither sign nor list keys.
451#[cfg(unix)]
452pub(crate) enum MaybeAuthorized {
453    /// The peer owns the agent and is served normally.
454    Authorized(AgentSession),
455    /// The peer is not the owner; every request is refused.
456    Denied,
457}
458
459#[cfg(unix)]
460#[ssh_agent_lib::async_trait]
461impl Session for MaybeAuthorized {
462    async fn request_identities(&mut self) -> Result<Vec<Identity>, SSHAgentError> {
463        match self {
464            Self::Authorized(session) => session.request_identities().await,
465            Self::Denied => Err(SSHAgentError::Failure),
466        }
467    }
468
469    async fn sign(&mut self, request: SignRequest) -> Result<Signature, SSHAgentError> {
470        match self {
471            Self::Authorized(session) => session.sign(request).await,
472            Self::Denied => Err(SSHAgentError::Failure),
473        }
474    }
475
476    async fn add_identity(&mut self, identity: AddIdentity) -> Result<(), SSHAgentError> {
477        match self {
478            Self::Authorized(session) => session.add_identity(identity).await,
479            Self::Denied => Err(SSHAgentError::Failure),
480        }
481    }
482
483    async fn remove_identity(&mut self, identity: RemoveIdentity) -> Result<(), SSHAgentError> {
484        match self {
485            Self::Authorized(session) => session.remove_identity(identity).await,
486            Self::Denied => Err(SSHAgentError::Failure),
487        }
488    }
489
490    async fn remove_all_identities(&mut self) -> Result<(), SSHAgentError> {
491        match self {
492            Self::Authorized(session) => session.remove_all_identities().await,
493            Self::Denied => Err(SSHAgentError::Failure),
494        }
495    }
496}
497
498/// Session factory that authorizes each incoming connection by peer UID before
499/// handing it an `AgentSession`.
500///
501/// `ssh_agent_lib` calls `new_session` once per accepted connection, giving access
502/// to the connecting socket. We read the peer's credentials there and refuse any
503/// connection that is not the owning user (failing closed if the credentials
504/// cannot be read).
505#[cfg(unix)]
506pub(crate) struct PeerAuthorizedAgent {
507    handle: Arc<AgentHandle>,
508    owner_uid: u32,
509    authorizer: Arc<dyn SignAuthorizer>,
510}
511
512#[cfg(unix)]
513impl PeerAuthorizedAgent {
514    /// Creates a factory that serves only connections from `owner_uid`.
515    ///
516    /// Args:
517    /// * `handle`: The agent handle that holds the unlocked keys.
518    /// * `owner_uid`: The effective user id permitted to use the agent.
519    ///
520    /// Usage:
521    /// ```ignore
522    /// let agent = PeerAuthorizedAgent::new(handle, owner_uid, authorizer);
523    /// ```
524    pub(crate) fn new(
525        handle: Arc<AgentHandle>,
526        owner_uid: u32,
527        authorizer: Arc<dyn SignAuthorizer>,
528    ) -> Self {
529        Self {
530            handle,
531            owner_uid,
532            authorizer,
533        }
534    }
535}
536
537#[cfg(unix)]
538impl Agent<tokio::net::UnixListener> for PeerAuthorizedAgent {
539    fn new_session(&mut self, socket: &tokio::net::UnixStream) -> impl Session {
540        match socket.peer_cred() {
541            Ok(cred) if peer_is_authorized(cred.uid(), self.owner_uid) => {
542                let peer = PeerIdentity {
543                    uid: cred.uid(),
544                    pid: cred.pid(),
545                };
546                MaybeAuthorized::Authorized(AgentSession::with_authorizer(
547                    self.handle.clone(),
548                    peer,
549                    self.authorizer.clone(),
550                ))
551            }
552            Ok(cred) => {
553                warn!(
554                    "Refusing agent connection from uid {} (agent owner uid {})",
555                    cred.uid(),
556                    self.owner_uid
557                );
558                MaybeAuthorized::Denied
559            }
560            Err(e) => {
561                error!("Refusing agent connection: cannot read peer credentials: {e}");
562                MaybeAuthorized::Denied
563            }
564        }
565    }
566}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571    use ring::rand::SystemRandom;
572    use ring::signature::Ed25519KeyPair;
573    use ssh_key::private::Ed25519Keypair as SshEd25519Keypair;
574    use std::path::PathBuf;
575    use zeroize::Zeroizing;
576
577    fn generate_test_pkcs8() -> Vec<u8> {
578        let rng = SystemRandom::new();
579        let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng).expect("Failed to generate PKCS#8");
580        pkcs8_doc.as_ref().to_vec()
581    }
582
583    /// A session whose per-request authorizer denies the peer must refuse to sign,
584    /// even with the agent unlocked and the key present. This is the #354 gate: an
585    /// unlocked agent does not grant silent signing to an unapproved same-user caller.
586    #[tokio::test]
587    async fn agent_refuses_to_sign_when_authorizer_denies() {
588        struct DenyAll;
589        impl SignAuthorizer for DenyAll {
590            fn authorize_sign(&self, _peer: &PeerIdentity) -> bool {
591                false
592            }
593        }
594
595        let seed: [u8; 32] = {
596            let pkcs8 = generate_test_pkcs8();
597            let mut s = [0u8; 32];
598            s.copy_from_slice(&pkcs8[16..48]);
599            s
600        };
601        let ssh_keypair = SshEd25519Keypair::from_seed(&seed);
602        let pubkey_bytes = ssh_keypair.public.0;
603
604        let handle = Arc::new(AgentHandle::new(PathBuf::from("/tmp/test-deny.sock")));
605        let peer = PeerIdentity {
606            uid: 1000,
607            pid: Some(4242),
608        };
609        let mut session = AgentSession::with_authorizer(handle.clone(), peer, Arc::new(DenyAll));
610
611        session
612            .add_identity(AddIdentity {
613                credential: Credential::Key {
614                    privkey: KeypairData::Ed25519(ssh_keypair),
615                    comment: "test-key".to_string(),
616                },
617            })
618            .await
619            .unwrap();
620        assert_eq!(handle.key_count().unwrap(), 1);
621
622        let request = SignRequest {
623            pubkey: KeyData::Ed25519(Ed25519PublicKey(pubkey_bytes)),
624            data: b"unauthorized payload".to_vec(),
625            flags: 0,
626        };
627        let result = session.sign(request).await;
628        assert!(
629            result.is_err(),
630            "a denied peer must not obtain a signature even when the agent is unlocked and the key is present"
631        );
632    }
633
634    /// The per-caller policy: an approved caller is pinned (not re-prompted), and a
635    /// *different* process — even at the same uid — triggers its own approval.
636    #[test]
637    fn per_caller_pins_approved_and_reprompts_a_different_process() {
638        use std::sync::atomic::{AtomicUsize, Ordering};
639
640        let prompts = Arc::new(AtomicUsize::new(0));
641        let p = prompts.clone();
642        let auth = PerCallerAuthorizer::new(move |peer: &PeerIdentity| {
643            p.fetch_add(1, Ordering::SeqCst);
644            peer.uid == 1000 // approve only the owner uid
645        });
646
647        let git = PeerIdentity {
648            uid: 1000,
649            pid: Some(11),
650        };
651        let malware = PeerIdentity {
652            uid: 1000,
653            pid: Some(22),
654        };
655        let stranger = PeerIdentity {
656            uid: 1001,
657            pid: Some(33),
658        };
659
660        assert!(
661            auth.authorize_sign(&git),
662            "first request from git is approved"
663        );
664        assert!(auth.authorize_sign(&git), "git is now pinned");
665        assert_eq!(
666            prompts.load(Ordering::SeqCst),
667            1,
668            "an approved caller is not re-prompted per signature"
669        );
670
671        assert!(
672            !auth.authorize_sign(&stranger),
673            "an unapproved peer is refused"
674        );
675        assert!(
676            auth.authorize_sign(&malware),
677            "a different same-uid process is approved by this fn, but only via its own prompt"
678        );
679        assert_eq!(
680            prompts.load(Ordering::SeqCst),
681            3,
682            "a different process triggers a fresh approval; pinning is per-(uid,pid)"
683        );
684    }
685
686    #[test]
687    fn test_agent_session_new() {
688        let handle = Arc::new(AgentHandle::new(PathBuf::from("/tmp/test.sock")));
689        let session = AgentSession::new(handle.clone());
690
691        assert_eq!(
692            session.handle().socket_path(),
693            &PathBuf::from("/tmp/test.sock")
694        );
695    }
696
697    #[test]
698    fn test_agent_session_clone_shares_handle() {
699        let handle = Arc::new(AgentHandle::new(PathBuf::from("/tmp/test.sock")));
700
701        let pkcs8_bytes = generate_test_pkcs8();
702        handle
703            .register_key(Zeroizing::new(pkcs8_bytes))
704            .expect("Failed to register key");
705
706        let session1 = AgentSession::new(handle.clone());
707        let session2 = session1.clone();
708
709        // Both sessions share the same handle
710        assert_eq!(session1.handle().key_count().unwrap(), 1);
711        assert_eq!(session2.handle().key_count().unwrap(), 1);
712    }
713
714    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
715    async fn test_add_identity_round_trip() {
716        // Generate a test seed and create an SSH keypair from it
717        let seed: [u8; 32] = {
718            let pkcs8 = generate_test_pkcs8();
719            // Extract seed from the PKCS#8 bytes (bytes 16..48 in ring's format)
720            let mut s = [0u8; 32];
721            s.copy_from_slice(&pkcs8[16..48]);
722            s
723        };
724
725        let ssh_keypair = SshEd25519Keypair::from_seed(&seed);
726        let pubkey_bytes = ssh_keypair.public.0;
727
728        // Build an AddIdentity request (what the client sends over the wire)
729        let identity = AddIdentity {
730            credential: Credential::Key {
731                privkey: KeypairData::Ed25519(ssh_keypair),
732                comment: "test-key".to_string(),
733            },
734        };
735
736        // Create session with empty handle
737        let handle = Arc::new(AgentHandle::new(PathBuf::from("/tmp/test-add.sock")));
738        let mut session = AgentSession::new(handle.clone());
739
740        // Verify no keys initially
741        assert_eq!(handle.key_count().unwrap(), 0);
742
743        // Add the identity via the session (this is what was broken before)
744        session.add_identity(identity).await.unwrap();
745
746        // Verify the key is now registered
747        assert_eq!(handle.key_count().unwrap(), 1);
748
749        // Sign data via the session and verify the signature
750        let sign_request = SignRequest {
751            pubkey: KeyData::Ed25519(Ed25519PublicKey(pubkey_bytes)),
752            data: b"test data for signing".to_vec(),
753            flags: 0,
754        };
755
756        let signature = session.sign(sign_request).await.unwrap();
757        assert_eq!(signature.algorithm(), Algorithm::Ed25519);
758        assert!(!signature.as_bytes().is_empty());
759
760        // Verify the signature using ring
761        let ring_pubkey =
762            ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, &pubkey_bytes);
763        ring_pubkey
764            .verify(b"test data for signing", signature.as_bytes())
765            .expect("Signature verification failed");
766    }
767
768    #[tokio::test]
769    async fn test_remove_identity() {
770        let handle = Arc::new(AgentHandle::new(PathBuf::from("/tmp/test-rm.sock")));
771        let mut session = AgentSession::new(handle.clone());
772
773        // Add a key via add_identity
774        let seed: [u8; 32] = {
775            let pkcs8 = generate_test_pkcs8();
776            let mut s = [0u8; 32];
777            s.copy_from_slice(&pkcs8[16..48]);
778            s
779        };
780        let ssh_keypair = SshEd25519Keypair::from_seed(&seed);
781        let pubkey_bytes = ssh_keypair.public.0;
782
783        let identity = AddIdentity {
784            credential: Credential::Key {
785                privkey: KeypairData::Ed25519(ssh_keypair),
786                comment: "test-key".to_string(),
787            },
788        };
789        session.add_identity(identity).await.unwrap();
790        assert_eq!(handle.key_count().unwrap(), 1);
791
792        // Remove it
793        let remove = RemoveIdentity {
794            pubkey: KeyData::Ed25519(Ed25519PublicKey(pubkey_bytes)),
795        };
796        session.remove_identity(remove).await.unwrap();
797        assert_eq!(handle.key_count().unwrap(), 0);
798    }
799
800    #[tokio::test]
801    async fn test_remove_all_identities() {
802        let handle = Arc::new(AgentHandle::new(PathBuf::from("/tmp/test-rmall.sock")));
803        let mut session = AgentSession::new(handle.clone());
804
805        // Add two keys
806        for _ in 0..2 {
807            let seed: [u8; 32] = {
808                let pkcs8 = generate_test_pkcs8();
809                let mut s = [0u8; 32];
810                s.copy_from_slice(&pkcs8[16..48]);
811                s
812            };
813            let ssh_keypair = SshEd25519Keypair::from_seed(&seed);
814            let identity = AddIdentity {
815                credential: Credential::Key {
816                    privkey: KeypairData::Ed25519(ssh_keypair),
817                    comment: "test-key".to_string(),
818                },
819            };
820            session.add_identity(identity).await.unwrap();
821        }
822        assert_eq!(handle.key_count().unwrap(), 2);
823
824        // Remove all
825        session.remove_all_identities().await.unwrap();
826        assert_eq!(handle.key_count().unwrap(), 0);
827    }
828
829    #[cfg(unix)]
830    fn registered_sign_request(handle: &Arc<AgentHandle>) -> SignRequest {
831        let pkcs8 = generate_test_pkcs8();
832        handle
833            .register_key(Zeroizing::new(pkcs8))
834            .expect("register key");
835        let pubkeys = handle.public_keys().expect("public keys");
836        let pubkey_bytes: [u8; 32] = pubkeys[0]
837            .as_slice()
838            .try_into()
839            .expect("ed25519 pubkey is 32 bytes");
840        SignRequest {
841            pubkey: KeyData::Ed25519(Ed25519PublicKey(pubkey_bytes)),
842            data: b"data to sign".to_vec(),
843            flags: 0,
844        }
845    }
846
847    #[test]
848    fn peer_authorized_only_for_matching_uid() {
849        assert!(peer_is_authorized(1000, 1000));
850        assert!(!peer_is_authorized(1001, 1000));
851        assert!(!peer_is_authorized(0, 1000));
852    }
853
854    #[cfg(unix)]
855    #[tokio::test]
856    async fn new_session_serves_owner_uid() {
857        use tokio::net::UnixStream;
858        let (a, _b) = UnixStream::pair().expect("socketpair");
859        let owner_uid = a.peer_cred().expect("peer cred").uid();
860
861        let handle = Arc::new(AgentHandle::new(PathBuf::from("/tmp/peer-allow.sock")));
862        let request = registered_sign_request(&handle);
863
864        let mut agent = PeerAuthorizedAgent::new(handle, owner_uid, Arc::new(AllowAllSigning));
865        let mut session = agent.new_session(&a);
866        assert!(
867            session.sign(request).await.is_ok(),
868            "the owning uid must be able to sign"
869        );
870    }
871
872    #[cfg(unix)]
873    #[tokio::test]
874    async fn new_session_denies_foreign_uid() {
875        use tokio::net::UnixStream;
876        let (a, _b) = UnixStream::pair().expect("socketpair");
877        let peer_uid = a.peer_cred().expect("peer cred").uid();
878
879        let handle = Arc::new(AgentHandle::new(PathBuf::from("/tmp/peer-deny.sock")));
880        let request = registered_sign_request(&handle);
881
882        // Owner is a different uid than the connecting peer: the connection must be denied
883        // even though a key is loaded and signing would otherwise succeed.
884        let mut agent =
885            PeerAuthorizedAgent::new(handle, peer_uid.wrapping_add(1), Arc::new(AllowAllSigning));
886        let mut session = agent.new_session(&a);
887        assert!(
888            session.sign(request).await.is_err(),
889            "a foreign-uid peer must not be able to sign"
890        );
891    }
892
893    #[cfg(unix)]
894    #[tokio::test]
895    async fn denied_session_refuses_every_request() {
896        let mut session = MaybeAuthorized::Denied;
897        assert!(session.request_identities().await.is_err());
898        let request = SignRequest {
899            pubkey: KeyData::Ed25519(Ed25519PublicKey([7u8; 32])),
900            data: b"x".to_vec(),
901            flags: 0,
902        };
903        assert!(session.sign(request).await.is_err());
904    }
905
906    #[cfg(unix)]
907    #[tokio::test]
908    async fn denied_session_refuses_every_request_variant() {
909        use ssh_agent_lib::proto::{
910            AddIdentityConstrained, AddSmartcardKeyConstrained, Extension, Request, Response,
911            SmartcardKey,
912        };
913
914        fn ed_keydata() -> KeyData {
915            KeyData::Ed25519(Ed25519PublicKey([0u8; 32]))
916        }
917        fn an_add_identity() -> AddIdentity {
918            AddIdentity {
919                credential: Credential::Key {
920                    privkey: KeypairData::Ed25519(SshEd25519Keypair::from_seed(&[0u8; 32])),
921                    comment: String::new(),
922                },
923            }
924        }
925        fn a_smartcard() -> SmartcardKey {
926            SmartcardKey {
927                id: String::new(),
928                pin: String::new().into(),
929            }
930        }
931
932        // Every request the SSH agent protocol defines. A denied connection must refuse
933        // all of them, so an upstream change that adds a permissive default cannot open
934        // a hole. Payloads are minimal — a denied session never inspects them.
935        let requests = vec![
936            Request::RequestIdentities,
937            Request::SignRequest(SignRequest {
938                pubkey: ed_keydata(),
939                data: Vec::new(),
940                flags: 0,
941            }),
942            Request::AddIdentity(an_add_identity()),
943            Request::RemoveIdentity(RemoveIdentity {
944                pubkey: ed_keydata(),
945            }),
946            Request::RemoveAllIdentities,
947            Request::AddSmartcardKey(a_smartcard()),
948            Request::RemoveSmartcardKey(a_smartcard()),
949            Request::Lock(String::new()),
950            Request::Unlock(String::new()),
951            Request::AddIdConstrained(AddIdentityConstrained {
952                identity: an_add_identity(),
953                constraints: Vec::new(),
954            }),
955            Request::AddSmartcardKeyConstrained(AddSmartcardKeyConstrained {
956                key: a_smartcard(),
957                constraints: Vec::new(),
958            }),
959            Request::Extension(Extension {
960                name: String::new(),
961                details: Vec::<u8>::new().into(),
962            }),
963        ];
964        assert_eq!(requests.len(), 12, "all agent request variants are covered");
965
966        for request in requests {
967            let mut session = MaybeAuthorized::Denied;
968            // Drive the real dispatcher and map its result the way the wire loop does.
969            let response = match session.handle(request).await {
970                Err(SSHAgentError::ExtensionFailure) => Response::ExtensionFailure,
971                Err(_) => Response::Failure,
972                Ok(r) => r,
973            };
974            assert!(
975                matches!(response, Response::Failure | Response::ExtensionFailure),
976                "a denied session must refuse every request variant, got {response:?}"
977            );
978        }
979    }
980
981    #[cfg(unix)]
982    #[tokio::test]
983    async fn sign_resets_idle_timer() {
984        use std::time::Duration;
985        let handle = Arc::new(AgentHandle::with_timeout(
986            PathBuf::from("/tmp/idle-touch.sock"),
987            Duration::from_millis(300),
988        ));
989        let request = registered_sign_request(&handle);
990        let mut session = AgentSession::new(handle.clone());
991
992        tokio::time::sleep(Duration::from_millis(200)).await;
993        session.sign(request).await.expect("sign should succeed");
994        tokio::time::sleep(Duration::from_millis(200)).await;
995
996        assert!(
997            !handle.is_idle_timed_out(),
998            "a successful sign must reset the idle timer so the agent stays unlocked"
999        );
1000    }
1001}