Skip to main content

auc_tool/application/
agent.rs

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}