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