1use std::fs;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4
5use anyhow::{Context, Result, anyhow, bail};
6use capulus::managed::PeerCredentials;
7use zbus::zvariant::OwnedObjectPath;
8
9use super::protocol::PROTOCOL_MAJOR;
10use super::{
11 ApplicationHandler, ApplicationRequest, ApplicationResponse, ErrorCode, ProtocolError, Status,
12};
13use crate::authenticator::PresenceGate;
14use crate::vault::Vault;
15
16const LOGIN_SERVICE: &str = "org.freedesktop.login1";
17const LOGIN_MANAGER_PATH: &str = "/org/freedesktop/login1";
18const LOGIN_MANAGER_INTERFACE: &str = "org.freedesktop.login1.Manager";
19const LOGIN_SESSION_INTERFACE: &str = "org.freedesktop.login1.Session";
20const LOGIN_USER_INTERFACE: &str = "org.freedesktop.login1.User";
21
22struct SessionProperties {
23 active: bool,
24 remote: bool,
25 class: String,
26 session_type: String,
27 uid: u32,
28}
29
30impl SessionProperties {
31 async fn load(connection: &zbus::Connection, path: &OwnedObjectPath) -> Result<Self> {
32 let session = zbus::Proxy::new(
33 connection,
34 LOGIN_SERVICE,
35 path.as_str(),
36 LOGIN_SESSION_INTERFACE,
37 )
38 .await
39 .context("failed to inspect the caller's logind session")?;
40 let (uid, _): (u32, OwnedObjectPath) = session.get_property("User").await?;
41 Ok(Self {
42 active: session.get_property("Active").await?,
43 remote: session.get_property("Remote").await?,
44 class: session.get_property("Class").await?,
45 session_type: session.get_property("Type").await?,
46 uid,
47 })
48 }
49
50 fn is_active_local_interactive(&self, uid: u32) -> bool {
51 self.uid == uid
52 && self.active
53 && !self.remote
54 && self.class == "user"
55 && matches!(self.session_type.as_str(), "tty" | "x11" | "wayland")
56 }
57}
58
59#[derive(Clone)]
60pub struct LocalSessionAuthorizer {
61 connection: zbus::Connection,
62}
63
64impl LocalSessionAuthorizer {
65 pub async fn connect() -> Result<Self> {
66 Ok(Self {
67 connection: zbus::Connection::system()
68 .await
69 .context("failed to connect to the system bus for logind authorization")?,
70 })
71 }
72
73 pub async fn authorize(&self, peer: PeerCredentials) -> Result<()> {
74 validate_process_uid(peer)?;
75 let manager = zbus::Proxy::new(
76 &self.connection,
77 LOGIN_SERVICE,
78 LOGIN_MANAGER_PATH,
79 LOGIN_MANAGER_INTERFACE,
80 )
81 .await
82 .context("failed to create the logind manager proxy")?;
83 match manager.call("GetSessionByPID", &peer.pid).await {
84 Ok(path) => {
85 if SessionProperties::load(&self.connection, &path)
86 .await?
87 .is_active_local_interactive(peer.uid)
88 {
89 return Ok(());
90 }
91 bail!("caller is not an active local interactive logind user");
92 }
93 Err(error) if no_session_for_pid(&error) => {}
94 Err(error) => {
95 return Err(error).context("failed to resolve the caller's logind session");
96 }
97 }
98 let user_path: OwnedObjectPath = manager
99 .call("GetUserByPID", &peer.pid)
100 .await
101 .context("the caller does not belong to a logind user manager")?;
102 let user = zbus::Proxy::new(
103 &self.connection,
104 LOGIN_SERVICE,
105 user_path.as_str(),
106 LOGIN_USER_INTERFACE,
107 )
108 .await
109 .context("failed to inspect the caller's logind user")?;
110 let uid: u32 = user.get_property("UID").await?;
111 if uid != peer.uid {
112 bail!("caller's logind user no longer matches its Unix socket credentials");
113 }
114 let sessions: Vec<(String, OwnedObjectPath)> = user.get_property("Sessions").await?;
115 for (_, path) in sessions {
116 if SessionProperties::load(&self.connection, &path)
117 .await?
118 .is_active_local_interactive(peer.uid)
119 {
120 return Ok(());
121 }
122 }
123 bail!("caller is not an active local interactive logind user")
124 }
125}
126
127fn no_session_for_pid(error: &zbus::Error) -> bool {
128 matches!(
129 error,
130 zbus::Error::MethodError(name, _, _)
131 if name.as_str() == "org.freedesktop.login1.NoSessionForPID"
132 )
133}
134
135pub struct AucApplication {
136 vault: Vault,
137 presence: PresenceGate,
138 device_present: Arc<AtomicBool>,
139 authorizer: LocalSessionAuthorizer,
140}
141
142impl AucApplication {
143 pub fn new(
144 vault: Vault,
145 presence: PresenceGate,
146 device_present: Arc<AtomicBool>,
147 authorizer: LocalSessionAuthorizer,
148 ) -> Self {
149 Self {
150 vault,
151 presence,
152 device_present,
153 authorizer,
154 }
155 }
156
157 async fn require_local_session(&self, peer: PeerCredentials) -> Result<(), ProtocolError> {
158 match crate::system::operator_is_authorized(peer.uid) {
159 Ok(true) => {}
160 Ok(false) => {
161 return Err(ProtocolError::new(
162 ErrorCode::Unauthorized,
163 "request requires an authorized auc operator",
164 ));
165 }
166 Err(error) => return Err(internal(error)),
167 }
168 self.authorizer.authorize(peer).await.map_err(|error| {
169 eprintln!("auc rejected application peer: {error:#}");
170 ProtocolError::new(
171 ErrorCode::Unauthorized,
172 "request requires an active local interactive login session",
173 )
174 })
175 }
176
177 fn status(&self) -> Result<ApplicationResponse, ProtocolError> {
178 self.vault
179 .credential_count()
180 .map(|credential_count| {
181 ApplicationResponse::Status(Status {
182 product: "auc".to_string(),
183 package: "auc-tool".to_string(),
184 version: env!("CARGO_PKG_VERSION").to_string(),
185 protocol_major: PROTOCOL_MAJOR,
186 device_present: self.device_present.load(Ordering::Acquire),
187 pending_touch: self.presence.has_pending_touch(),
188 credential_count,
189 })
190 })
191 .map_err(internal)
192 }
193}
194
195impl ApplicationHandler for AucApplication {
196 async fn handle(
197 &self,
198 peer: PeerCredentials,
199 request: ApplicationRequest,
200 ) -> Result<ApplicationResponse, ProtocolError> {
201 match request {
202 ApplicationRequest::Status => self.status(),
203 ApplicationRequest::Touch => {
204 self.require_local_session(peer).await?;
205 self.presence
206 .touch()
207 .map(ApplicationResponse::Touch)
208 .map_err(|_| {
209 ProtocolError::new(
210 ErrorCode::Conflict,
211 "no auc operation is waiting for presence",
212 )
213 })
214 }
215 ApplicationRequest::ListCredentials => {
216 self.require_local_session(peer).await?;
217 self.vault
218 .credential_summaries()
219 .map(|credentials| ApplicationResponse::Credentials { credentials })
220 .map_err(internal)
221 }
222 ApplicationRequest::DeleteCredential { credential_id } => {
223 self.require_local_session(peer).await?;
224 let credential = decode_credential_id(&credential_id)?;
225 match self
226 .vault
227 .delete_credential(&credential)
228 .map_err(internal)?
229 {
230 true => Ok(ApplicationResponse::Deleted { credential_id }),
231 false => Err(ProtocolError::new(
232 ErrorCode::NotFound,
233 "credential was not found",
234 )),
235 }
236 }
237 ApplicationRequest::Unknown => Err(ProtocolError::new(
238 ErrorCode::BadRequest,
239 "auc application method is not supported",
240 )),
241 }
242 }
243}
244
245fn validate_process_uid(peer: PeerCredentials) -> Result<()> {
246 if peer.pid == 0 {
247 bail!("peer supplied an invalid kernel PID");
248 }
249 let status = fs::read_to_string(format!("/proc/{}/status", peer.pid))
250 .context("failed to inspect the application peer process")?;
251 let uid_line = status
252 .lines()
253 .find_map(|line| line.strip_prefix("Uid:"))
254 .ok_or_else(|| anyhow!("peer process status has no UID record"))?;
255 let uids = uid_line
256 .split_whitespace()
257 .map(str::parse::<u32>)
258 .collect::<std::result::Result<Vec<_>, _>>()?;
259 if uids.len() != 4 || uids.iter().any(|uid| *uid != peer.uid) {
260 bail!("peer process UID no longer matches its Unix socket credentials");
261 }
262 Ok(())
263}
264
265fn decode_credential_id(value: &str) -> Result<Vec<u8>, ProtocolError> {
266 if value.is_empty()
267 || value.len() > 2048
268 || !value.len().is_multiple_of(2)
269 || !value.bytes().all(|byte| byte.is_ascii_hexdigit())
270 {
271 return Err(ProtocolError::new(
272 ErrorCode::BadRequest,
273 "credential ID must be lowercase hexadecimal",
274 ));
275 }
276 if value.bytes().any(|byte| byte.is_ascii_uppercase()) {
277 return Err(ProtocolError::new(
278 ErrorCode::BadRequest,
279 "credential ID must be lowercase hexadecimal",
280 ));
281 }
282 hex::decode(value).map_err(|_| {
283 ProtocolError::new(
284 ErrorCode::BadRequest,
285 "credential ID must be lowercase hexadecimal",
286 )
287 })
288}
289
290fn internal(error: anyhow::Error) -> ProtocolError {
291 eprintln!("auc application operation failed: {error:#}");
292 ProtocolError::new(ErrorCode::Internal, "auc application operation failed")
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 fn session(active: bool, remote: bool, class: &str, session_type: &str) -> SessionProperties {
300 SessionProperties {
301 active,
302 remote,
303 class: class.to_string(),
304 session_type: session_type.to_string(),
305 uid: 1000,
306 }
307 }
308
309 #[test]
310 fn credential_ids_are_canonical_and_bounded() {
311 assert_eq!(
312 decode_credential_id("deadbeef").unwrap(),
313 [0xde, 0xad, 0xbe, 0xef]
314 );
315 assert!(decode_credential_id("").is_err());
316 assert!(decode_credential_id("DEADBEEF").is_err());
317 assert!(decode_credential_id("abc").is_err());
318 assert!(decode_credential_id(&"aa".repeat(1025)).is_err());
319 }
320
321 #[test]
322 fn only_active_local_interactive_user_sessions_supply_presence() {
323 assert!(session(true, false, "user", "wayland").is_active_local_interactive(1000));
324 assert!(session(true, false, "user", "x11").is_active_local_interactive(1000));
325 assert!(session(true, false, "user", "tty").is_active_local_interactive(1000));
326 assert!(!session(false, false, "user", "wayland").is_active_local_interactive(1000));
327 assert!(!session(true, true, "user", "tty").is_active_local_interactive(1000));
328 assert!(!session(true, false, "manager", "unspecified").is_active_local_interactive(1000));
329 assert!(!session(true, false, "user", "wayland").is_active_local_interactive(1001));
330 }
331}