1pub use crate::agent::SSHAgent;
2use crate::filter::IdentityFilter;
3use crate::verify::verify;
4use anyhow::{anyhow, Result};
5use log::{debug, info};
6use ssh_agent_client_rs::{Error as SACError, Identity};
7use ssh_key::HashAlg;
8use std::time::{SystemTime, UNIX_EPOCH};
9use Identity::{Certificate, PublicKey};
10
11const CHALLENGE_SIZE: usize = 32;
12
13pub fn authenticate(
20 filter: &IdentityFilter,
21 mut agent: impl SSHAgent,
22 principal: &str,
23) -> Result<bool> {
24 for identity in agent.list_identities()? {
25 if filter.filter(&identity) {
26 if let Certificate(cert) = &identity {
27 if !validate_cert(cert, SystemTime::now(), principal) {
28 info!("Cert not valid, skipping");
29 continue;
30 }
31 }
32 match sign_and_verify(identity, &mut agent) {
35 Ok(res) => return Ok(res),
36 Err(e) => {
37 if let Some(SACError::RemoteFailure) = e.downcast_ref::<SACError>() {
38 debug!("SSHAgent: RemoteFailure; trying next key");
39 continue;
40 } else {
41 return Err(e);
42 }
43 }
44 }
45 }
46 }
47 Ok(false)
48}
49
50fn sign_and_verify(identity: Identity<'static>, agent: &mut impl SSHAgent) -> Result<bool> {
51 let mut data: [u8; CHALLENGE_SIZE] = [0_u8; CHALLENGE_SIZE];
52 getrandom::fill(data.as_mut_slice()).map_err(|_| anyhow!("Failed to obtain random data"))?;
53 let sig = agent.sign(identity.clone(), &data)?;
54 match identity {
55 PublicKey(key) => verify(key.key_data(), &data, &sig)?,
56 Certificate(cert) => verify(cert.public_key(), &data, &sig)?,
57 };
58 Ok(true)
59}
60
61fn validate_cert(cert: &ssh_key::Certificate, when: SystemTime, principal: &str) -> bool {
62 let ca_key = cert.signature_key();
63
64 if let Err(e) = cert.validate_at(
65 when.duration_since(UNIX_EPOCH)
66 .expect("Time went backwards")
67 .as_secs(),
68 vec![&ca_key.fingerprint(HashAlg::Sha256)],
69 ) {
70 info!("Certificate validation failed: {e:?}");
71 return false;
72 }
73
74 if !cert.valid_principals().iter().any(|p| p == principal) {
75 info!("Cert matches but '{principal}' is not in the list of valid principals.");
76 return false;
77 }
78
79 if !cert.critical_options().is_empty() {
80 info!("Cert has critical options we don't know how to handle");
81 return false;
82 }
83
84 true
85}
86
87#[cfg(test)]
88mod test {
89 use crate::auth::validate_cert;
90 use crate::test::{data, CERT_STR};
91 use anyhow::Result;
92 use ssh_key::Certificate;
93 use std::time::{Duration, SystemTime};
94
95 #[test]
96 fn test_parse_cert() -> Result<()> {
97 let cert = Certificate::from_openssh(CERT_STR)?;
98 assert!(validate_cert(&cert, st(1752577200), "principal"));
100 assert!(!validate_cert(&cert, st(1752577200), "another"));
102 assert!(!validate_cert(&cert, st(1749985200), "principal"));
104 assert!(!validate_cert(&cert, st(1755255600), "principal"));
106
107 let mut bytes = CERT_STR.as_bytes().to_vec();
109 bytes[90] = 0x42;
110 let cert = Certificate::from_openssh(&String::from_utf8_lossy(bytes.as_slice()))?;
111 assert!(!validate_cert(&cert, st(1752577200), "principal"));
113
114 Ok(())
115 }
116
117 #[test]
118 fn test_unknown_critical_field_in_cert() -> Result<()> {
119 let cert = Certificate::from_openssh(include_str!(data!("cert_unknown_critical.pub")))?;
120 assert!(!validate_cert(&cert, st(934714800), "user"));
122 Ok(())
123 }
124
125 fn st(timestamp: u64) -> SystemTime {
126 SystemTime::UNIX_EPOCH + Duration::from_secs(timestamp)
127 }
128}