use std::ffi::OsString; use std::io::{self, Read, Write}; use std::os::unix::ffi::OsStringExt as _; use std::path::PathBuf; use std::process::ExitCode; use std::time::Duration; use ed25519_dalek::SigningKey; use lazaret_core::codec::decode; use lazaret_core::protocol::VerifiedSession; use lazaret_core::records::{ EscalationOutcome, ProtocolPublicationIdentity, SandboxCredentialBundle, SessionBinding, SessionRegistrationResponse, }; use lazaret_escalation_client::{ClientEvent, EscalationClient}; use lazaret_linux::filesystem::RegularFileMailbox; struct Arguments { inbox: PathBuf, outbox: PathBuf, publication: PathBuf, sandbox_credentials: PathBuf, registration: PathBuf, session_binding: PathBuf, justification: String, command_argv: Vec>, } fn main() -> ExitCode { match run() { Ok(code) => ExitCode::from(code), Err(error) => { eprintln!("lazaret escalation refused: {error}"); ExitCode::from(2) } } } fn run() -> Result> { let arguments = parse_arguments(std::env::args_os().skip(1).collect())?; let publication: ProtocolPublicationIdentity = read_record(&arguments.publication)?; let bundle: SandboxCredentialBundle = read_record(&arguments.sandbox_credentials)?; let registration: SessionRegistrationResponse = read_record(&arguments.registration)?; let binding: SessionBinding = read_record(&arguments.session_binding)?; let sandbox_key_bytes: [u8; 32] = bundle.sandbox_private_key.as_slice().try_into()?; let sandbox_key = SigningKey::from_bytes(&sandbox_key_bytes); let session = VerifiedSession::new( publication.trust_manifest_sha256, bundle.trust_manifest.tls_ca_sha256, &bundle.trust_manifest, binding, bundle.coordinator_protocol_credential(), bundle.sandbox_protocol_credential(), registration.session_capability, )?; let client = EscalationClient::new( RegularFileMailbox::open(&arguments.inbox)?, RegularFileMailbox::open(&arguments.outbox)?, session, sandbox_key, ); let mut request = client.request(arguments.command_argv, arguments.justification)?; loop { match request.poll()? { ClientEvent::Output { stream, bytes, .. } => match stream { lazaret_core::records::ResultStream::Stdout => { io::stdout().write_all(&bytes)?; io::stdout().flush()?; } lazaret_core::records::ResultStream::Stderr => { io::stderr().write_all(&bytes)?; io::stderr().flush()?; } }, ClientEvent::Terminal(result) => { return Ok(match result.outcome { EscalationOutcome::Completed => result .exit_code .and_then(|code| u8::try_from(code).ok()) .unwrap_or(1), EscalationOutcome::Denied => 3, EscalationOutcome::TimedOut => 4, EscalationOutcome::Cancelled => 5, EscalationOutcome::Failed => 6, }); } ClientEvent::Pending => std::thread::sleep(Duration::from_millis(10)), } } } fn read_record(path: &std::path::Path) -> Result> where T: serde::de::DeserializeOwned + serde::Serialize, { let mut file = std::fs::File::from(rustix::fs::openat( rustix::fs::CWD, path, rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::NONBLOCK | rustix::fs::OFlags::NOFOLLOW | rustix::fs::OFlags::CLOEXEC, rustix::fs::Mode::empty(), )?); let metadata = file.metadata()?; let length = usize::try_from(metadata.len())?; if !metadata.file_type().is_file() || length > lazaret_core::codec::MAX_SCHEMA_RECORD_BYTES { return Err(io::Error::other("client authority record type or size is invalid").into()); } let mut bytes = Vec::with_capacity(length.saturating_add(1)); Read::by_ref(&mut file) .take((lazaret_core::codec::MAX_SCHEMA_RECORD_BYTES as u64) + 1) .read_to_end(&mut bytes)?; if bytes.len() != length || file.metadata()?.len() != metadata.len() { return Err(io::Error::other("client authority record changed while read").into()); } Ok(decode(&bytes)?) } fn parse_arguments(mut arguments: Vec) -> Result { let separator = arguments .iter() .position(|argument| argument == "--") .ok_or_else(|| io::Error::other("command separator is missing"))?; let command = arguments.split_off(separator + 1); arguments.pop(); if command.is_empty() { return Err(io::Error::other("command argv is empty")); } let mut inbox = None; let mut outbox = None; let mut publication = None; let mut sandbox_credentials = None; let mut registration = None; let mut session_binding = None; let mut justification = None; let mut options = arguments.into_iter(); while let Some(option) = options.next() { let value = options .next() .ok_or_else(|| io::Error::other("client option has no value"))?; match option.to_str() { Some("--inbox") => inbox = Some(PathBuf::from(value)), Some("--outbox") => outbox = Some(PathBuf::from(value)), Some("--publication") => publication = Some(PathBuf::from(value)), Some("--sandbox-credentials") => sandbox_credentials = Some(PathBuf::from(value)), Some("--registration") => registration = Some(PathBuf::from(value)), Some("--session-binding") => session_binding = Some(PathBuf::from(value)), Some("--justification") => { justification = Some( value .into_string() .map_err(|_| io::Error::other("justification is not UTF-8"))?, ); } _ => return Err(io::Error::other("unknown client option")), } } Ok(Arguments { inbox: inbox.ok_or_else(|| io::Error::other("inbox path is missing"))?, outbox: outbox.ok_or_else(|| io::Error::other("outbox path is missing"))?, publication: publication.ok_or_else(|| io::Error::other("publication path is missing"))?, sandbox_credentials: sandbox_credentials .ok_or_else(|| io::Error::other("sandbox credentials path is missing"))?, registration: registration .ok_or_else(|| io::Error::other("registration path is missing"))?, session_binding: session_binding .ok_or_else(|| io::Error::other("session binding path is missing"))?, justification: justification.ok_or_else(|| io::Error::other("justification is missing"))?, command_argv: command.into_iter().map(OsString::into_vec).collect(), }) } #[cfg(test)] mod tests { use super::parse_arguments; use std::ffi::OsString; fn valid_arguments() -> Vec { [ "--inbox", "/inbox", "--outbox", "/outbox", "--publication", "/publication", "--sandbox-credentials", "/credentials", "--registration", "/registration", "--session-binding", "/binding", "--justification", "needed", "--", "/usr/bin/tool", "$HOME", "a;b", ] .into_iter() .map(OsString::from) .collect() } #[test] fn parser_preserves_exact_argv_without_shell_interpretation() { let parsed = parse_arguments(valid_arguments()).unwrap(); assert_eq!( parsed.command_argv, [ b"/usr/bin/tool".to_vec(), b"$HOME".to_vec(), b"a;b".to_vec() ] ); } #[test] fn parser_refuses_missing_separator_unknown_options_and_empty_argv() { let mut missing_separator = valid_arguments(); missing_separator.retain(|argument| argument != "--"); assert!(parse_arguments(missing_separator).is_err()); let mut unknown = valid_arguments(); unknown[0] = OsString::from("--unknown"); assert!(parse_arguments(unknown).is_err()); let mut empty = valid_arguments(); empty.truncate(empty.iter().position(|argument| argument == "--").unwrap() + 1); assert!(parse_arguments(empty).is_err()); } }