auc-tool 0.1.0

Machine-local software passkey authenticator exposed through Linux UHID.
Documentation
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
use std::time::Instant;

use anyhow::{Context, Result, anyhow, bail};
use capulus::managed::{
    AgentInfo, AgentServiceOptions, ApplicationSocketOptions, ManagedProduct,
    ManagedProductOptions, ManagedRedeployOptions, ManagementClient, ManagementClientOptions,
    ManagementRequest, ManagementResponse, ServiceHardening, SocketOptions, SystemBinary,
    UserBinary,
};
use semver::Version;

pub const APPLICATION_SOCKET_PATH: &str = "/run/auc/agent.sock";
pub const MANAGEMENT_SOCKET_PATH: &str = "/run/auc/capulus.sock";
pub const ACCESS_GROUP: &str = "auc";
const READINESS_RETRY: Duration = Duration::from_secs(1);

pub fn managed_product() -> Result<ManagedProduct> {
    ManagedProductOptions {
        product: "auc".to_string(),
        package: "auc-tool".to_string(),
        version: Version::parse(env!("CARGO_PKG_VERSION"))
            .context("auc package version is not semantic")?,
        system_binaries: vec![
            SystemBinary {
                cargo_name: "auc".to_string(),
                destination: PathBuf::from("/usr/local/bin/auc"),
            },
            SystemBinary {
                cargo_name: "auc-agent".to_string(),
                destination: PathBuf::from("/usr/local/bin/auc-agent"),
            },
        ],
        user_binary: UserBinary {
            cargo_name: "auc".to_string(),
        },
        agent_binary: PathBuf::from("/usr/local/bin/auc-agent"),
        service: AgentServiceOptions {
            description: "auc machine-local passkey authenticator".to_string(),
            executable: PathBuf::from("/usr/local/bin/auc-agent"),
            arguments: vec!["serve".to_string()],
            restart_delay: Duration::from_secs(2),
            network_required: false,
            state_directory_mode: 0o700,
            hardening: ServiceHardening::Strict {
                read_write_paths: vec![PathBuf::from("/var/lib/auc")],
                device_allow: vec![PathBuf::from("/dev/uhid")],
            },
        },
        application_socket: ApplicationSocketOptions::SystemdActivated(SocketOptions {
            path: PathBuf::from(APPLICATION_SOCKET_PATH),
            mode: 0o660,
            group: Some(ACCESS_GROUP.to_string()),
        }),
        management_socket: SocketOptions {
            path: PathBuf::from(MANAGEMENT_SOCKET_PATH),
            mode: 0o660,
            group: Some(ACCESS_GROUP.to_string()),
        },
        redeploy: ManagedRedeployOptions::default(),
    }
    .validate()
    .context("auc managed-product declaration is invalid")
}

pub fn application_agent_info() -> Result<AgentInfo> {
    use crate::application::{ApplicationClient, ApplicationRequest, ApplicationResponse};

    match ApplicationClient::new(APPLICATION_SOCKET_PATH).request(ApplicationRequest::Status)? {
        ApplicationResponse::Status(status) => {
            if !status.device_present {
                bail!("auc UHID device is not present");
            }
            Ok(AgentInfo {
                product: status.product,
                package: status.package,
                version: status.version,
                protocol_major: status.protocol_major,
            })
        }
        _ => Err(anyhow!(
            "auc application health request returned the wrong response"
        )),
    }
}

pub fn wait_until_healthy(expected_version: &Version, timeout: Duration) -> Result<()> {
    let product = managed_product()?;
    let mut options = ManagementClientOptions::new(MANAGEMENT_SOCKET_PATH);
    options.timeout = Duration::from_secs(2);
    let management = ManagementClient::new(options);
    let started = Instant::now();
    let mut last_error = None;
    while started.elapsed() < timeout {
        let result = management
            .request(ManagementRequest::Info)
            .map_err(anyhow::Error::from)
            .and_then(|response| match response {
                ManagementResponse::Info(info) => Ok(info),
                _ => Err(anyhow!(
                    "auc management health request returned the wrong response"
                )),
            })
            .and_then(|management| {
                let application = application_agent_info()?;
                for info in [&management, &application] {
                    if info.product != product.name()
                        || info.package != product.package()
                        || info.version != expected_version.to_string()
                        || info.protocol_major != capulus::managed::PROTOCOL_MAJOR
                    {
                        bail!("auc agent health identity or version does not match");
                    }
                }
                Ok(())
            });
        match result {
            Ok(()) => return Ok(()),
            Err(error) => last_error = Some(error),
        }
        thread::sleep(READINESS_RETRY);
    }
    Err(last_error
        .unwrap_or_else(|| anyhow!("auc readiness deadline elapsed without a response"))
        .context("auc did not become healthy on both sockets"))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn manifest_has_one_agent_and_two_distinct_sockets() {
        let product = managed_product().unwrap();
        assert_eq!(product.service_name(), "auc-agent.service");
        assert_eq!(product.application_socket_name(), "auc-agent.socket");
        assert_eq!(product.management_socket_name(), "auc-capulus.socket");
        assert_ne!(
            product.application_socket_path(),
            product.management_socket_path()
        );
        product
            .installation_manifest()
            .validate(product.name())
            .unwrap();
    }
}