1use crate::cmd;
2use crate::environment::get_uid;
3use anyhow::Result;
4use anyhow::anyhow;
5use log::{debug, info};
6use ssh_agent_client_rs::Identity;
7use ssh_agent_client_rs::Identity::{Certificate, PublicKey};
8use ssh_key::AuthorizedKeys;
9use ssh_key::public::KeyData;
10use std::collections::HashSet;
11use std::fs;
12use std::path::Path;
13use std::time::Duration;
14use uzers::uid_t;
15
16pub struct IdentityFilter {
20 keys: HashSet<KeyData>,
21 ca_keys: HashSet<KeyData>,
22}
23
24impl IdentityFilter {
25 pub fn new(
32 authorized_keys_file: &Path,
33 ca_keys_file: Option<&Path>,
34 authorized_keys_command: Option<&str>,
35 authorized_keys_command_user: Option<&str>,
36 calling_user: &str,
37 ) -> Result<Self> {
38 let mut identities = Vec::new();
39 if authorized_keys_file.exists() {
40 identities.extend(from_file(authorized_keys_file, false)?);
41 } else if ca_keys_file.is_none() && authorized_keys_command.is_none() {
42 info!("No valid keys for authentication, {authorized_keys_file:?} does not exist");
43 }
44
45 if let Some(ca_keys_file) = ca_keys_file {
46 identities.extend(from_file(ca_keys_file, true)?);
47 }
48
49 if let Some(cmd) = authorized_keys_command {
50 let user = authorized_keys_command_user.unwrap_or(calling_user);
51 identities.extend(from_command(cmd, get_uid(user)?, calling_user)?);
52 }
53 Self::from(identities)
54 }
55
56 pub fn from_authorized_file(authorized_keys_file: &Path) -> Result<Self> {
57 Self::new(authorized_keys_file, None, None, None, "")
58 }
59
60 fn from(authorized: Vec<Authorized>) -> Result<Self> {
61 let mut keys: HashSet<KeyData> = HashSet::new();
62 let mut ca_keys: HashSet<KeyData> = HashSet::new();
63
64 for item in authorized {
65 match item {
66 Authorized::Key(key) => keys.insert(key),
67 Authorized::CAKey(ca_key) => ca_keys.insert(ca_key),
68 };
69 }
70
71 Ok(Self { keys, ca_keys })
72 }
73
74 pub fn filter(&self, identity: &Identity) -> bool {
79 match identity {
80 PublicKey(key) => {
81 if self.keys.contains(key.key_data()) {
82 debug!(
83 "found a matching key: {}",
84 key.fingerprint(Default::default())
85 );
86 return true;
87 }
88 }
89 Certificate(cert) => {
90 let ca_key = cert.signature_key();
91 if self.ca_keys.contains(ca_key) {
92 debug!(
93 "found a matching cert-authority key: {}",
94 ca_key.fingerprint(Default::default())
95 );
96 return true;
97 }
98 }
99 }
100 false
101 }
102}
103
104enum Authorized {
105 Key(KeyData),
106 CAKey(KeyData),
107}
108const MAX_DURATION: Duration = Duration::from_secs(10);
109
110fn from_command(command: &str, uid: uid_t, calling_user: &str) -> Result<Vec<Authorized>> {
111 debug!(
112 "Invoking command '{command} {calling_user}' to obtain public keys for user {calling_user}"
113 );
114 let buf = cmd::run(&[command, calling_user], MAX_DURATION, uid, None)?;
115 from_str(&buf, &format!("{command}:(output):"), false)
116}
117
118fn from_file(filename: &Path, ca_keys: bool) -> Result<Vec<Authorized>> {
119 let contents = fs::read_to_string(filename)?;
120 from_str(
121 &contents,
122 filename.to_str().ok_or(anyhow!("invalid filename"))?,
123 ca_keys,
124 )
125}
126
127fn from_str(buf: &str, what: &str, ca_keys: bool) -> Result<Vec<Authorized>> {
128 let keys: AuthorizedKeys = AuthorizedKeys::new(buf);
129 let iter = keys.enumerate().filter_map(move |(i, ak)| match ak {
130 Ok(entry) => {
131 let key_data = entry.public_key().key_data().to_owned();
132 if !ca_keys && !entry.config_opts().iter().any(|o| o == "cert-authority") {
133 return Some(Authorized::Key(key_data));
134 }
135 Some(Authorized::CAKey(key_data))
136 }
137 Err(e) => {
138 info!("Failed to parse line {what}:{i}': {e}");
139 None
140 }
141 });
142 Ok(iter.collect())
143}
144
145#[cfg(test)]
146mod tests {
147 use crate::filter::IdentityFilter;
148 use crate::test::{CERT_STR, data};
149 use ssh_agent_client_rs::Identity;
150 use ssh_key::{Certificate, PublicKey};
151 use std::env;
152 use std::path::Path;
153
154 #[test]
155 fn test_read_public_keys() -> anyhow::Result<()> {
156 let path = Path::new(data!("authorized_keys"));
157
158 let filter = IdentityFilter::from_authorized_file(path)?;
159
160 let cert = Certificate::from_openssh(CERT_STR)?;
162 let identity: Identity = cert.into();
163 assert!(filter.filter(&identity));
164
165 let filter = IdentityFilter::new(
168 Path::new("/dev/null"),
170 Some(Path::new(data!("ca_key.pub"))),
171 None,
172 None,
173 "",
174 )?;
175 assert!(filter.filter(&identity));
176
177 let filter = IdentityFilter::new(
179 Path::new("/does/not/exist"),
181 Some(Path::new(data!("ca_key.pub"))),
182 None,
183 None,
184 "",
185 )?;
186 assert!(filter.filter(&identity));
187
188 Ok(())
189 }
190
191 #[test]
194 #[ignore]
195 fn test_invoke_command_for_public_keys() -> anyhow::Result<()> {
196 let filter = IdentityFilter::new(
197 Path::new("/dev/null"),
198 None,
199 Some(data!("test.sh")),
200 None,
201 &env::var("USER")?,
202 )?;
203 let identity: Identity =
204 PublicKey::from_openssh(include_str!(data!("id_ed25519.pub")))?.into();
205 assert!(filter.filter(&identity));
206 Ok(())
207 }
208}