Integrate host escalation with production sessions
This commit is contained in:
parent
254bb37764
commit
696dae610a
3 changed files with 1175 additions and 96 deletions
|
|
@ -14,6 +14,7 @@ ed25519-dalek.workspace = true
|
|||
getrandom.workspace = true
|
||||
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
|
||||
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
|
||||
nix.workspace = true
|
||||
rustix.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ use lazaret_core::protocol::{
|
|||
encode_signed, request_file_name, result_chunk_file_name, result_file_name,
|
||||
};
|
||||
use lazaret_core::records::{
|
||||
MailboxCancellation, MailboxRequest, MailboxRequestError, MailboxResult, MailboxResultAck,
|
||||
ProtocolVersion, ResultStream,
|
||||
EscalationOutcome, MailboxCancellation, MailboxRequest, MailboxRequestError, MailboxResult,
|
||||
MailboxResultAck, ProtocolVersion, ResultStream,
|
||||
};
|
||||
use lazaret_linux::filesystem::RegularFileMailbox;
|
||||
|
||||
|
|
@ -23,6 +23,11 @@ pub enum ClientError {
|
|||
Window(ResultWindowError),
|
||||
Random(getrandom::Error),
|
||||
TerminalAlreadyReceived,
|
||||
TerminalChunkCount {
|
||||
received: u64,
|
||||
committed: u64,
|
||||
outcome: EscalationOutcome,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for ClientError {
|
||||
|
|
@ -37,6 +42,14 @@ impl fmt::Display for ClientError {
|
|||
Self::TerminalAlreadyReceived => {
|
||||
formatter.write_str("escalation request already received a terminal result")
|
||||
}
|
||||
Self::TerminalChunkCount {
|
||||
received,
|
||||
committed,
|
||||
outcome,
|
||||
} => write!(
|
||||
formatter,
|
||||
"terminal {outcome:?} commits {committed} chunks after {received} were received"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +63,7 @@ impl std::error::Error for ClientError {
|
|||
Self::ProtocolCodec(error) => Some(error),
|
||||
Self::Window(error) => Some(error),
|
||||
Self::Random(error) => Some(error),
|
||||
Self::TerminalAlreadyReceived => None,
|
||||
Self::TerminalAlreadyReceived | Self::TerminalChunkCount { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -128,7 +141,7 @@ impl EscalationClient {
|
|||
let signed = encode_signed(&request, &self.sandbox_key)?;
|
||||
let (_, digest) = self.session.verify_request(&signed)?;
|
||||
self.inbox
|
||||
.publish(&request_file_name(request_id), &signed)?;
|
||||
.publish_replacing(&request_file_name(request_id), &signed)?;
|
||||
Ok(ClientRequest {
|
||||
client: self,
|
||||
request_id,
|
||||
|
|
@ -178,7 +191,7 @@ impl ClientRequest<'_> {
|
|||
let signed = encode_signed(&cancellation, &self.client.sandbox_key)?;
|
||||
self.client
|
||||
.inbox
|
||||
.publish(&cancellation_file_name(self.request_id), &signed)?;
|
||||
.publish_replacing(&cancellation_file_name(self.request_id), &signed)?;
|
||||
self.cancellation_published = true;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -217,7 +230,7 @@ impl ClientRequest<'_> {
|
|||
highest_contiguous_sequence: chunk.sequence,
|
||||
};
|
||||
let signed = encode_signed(&acknowledgment, &self.client.sandbox_key)?;
|
||||
self.client.inbox.publish(
|
||||
self.client.inbox.publish_replacing(
|
||||
&acknowledgment_file_name(self.request_id, chunk.sequence),
|
||||
&signed,
|
||||
)?;
|
||||
|
|
@ -230,14 +243,21 @@ impl ClientRequest<'_> {
|
|||
}
|
||||
let result_name = result_file_name(self.request_id);
|
||||
if self.client.outbox.names()?.contains(&result_name) {
|
||||
let encoded = self
|
||||
let (encoded, _) = self
|
||||
.client
|
||||
.outbox
|
||||
.consume(&result_name, MAX_SIGNED_RECORD_BYTES)?;
|
||||
.read(&result_name, MAX_SIGNED_RECORD_BYTES)?;
|
||||
let result =
|
||||
self.client
|
||||
.session
|
||||
.verify_result(&encoded, self.request_id, self.digest)?;
|
||||
if result.chunk_count != self.collector.received_count() {
|
||||
return Err(ClientError::TerminalChunkCount {
|
||||
received: self.collector.received_count(),
|
||||
committed: result.chunk_count,
|
||||
outcome: result.outcome,
|
||||
});
|
||||
}
|
||||
self.collector.finish(&result)?;
|
||||
self.terminal = true;
|
||||
return Ok(ClientEvent::Terminal(Box::new(result)));
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ use lazaret_core::records::{
|
|||
};
|
||||
use lazaret_escalation_client::{ClientEvent, EscalationClient};
|
||||
use lazaret_linux::filesystem::RegularFileMailbox;
|
||||
use nix::sys::signal::{SigSet, SigmaskHow, Signal};
|
||||
use nix::sys::signalfd::{SfdFlags, SignalFd};
|
||||
|
||||
struct Arguments {
|
||||
inbox: PathBuf,
|
||||
|
|
@ -37,12 +39,23 @@ fn main() -> ExitCode {
|
|||
}
|
||||
|
||||
fn run() -> Result<u8, Box<dyn std::error::Error>> {
|
||||
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 arguments = parse_arguments_with_session(
|
||||
std::env::args_os().skip(1).collect(),
|
||||
std::env::var_os("LAZARET_SESSION_ID"),
|
||||
)?;
|
||||
let publication: ProtocolPublicationIdentity =
|
||||
read_record(&arguments.publication).map_err(|error| context("publication", error))?;
|
||||
let bundle: SandboxCredentialBundle = read_record(&arguments.sandbox_credentials)
|
||||
.map_err(|error| context("sandbox credentials", error))?;
|
||||
let registration: SessionRegistrationResponse = read_record(&arguments.registration)
|
||||
.map_err(|error| context("session registration", error))?;
|
||||
let binding: SessionBinding = read_record(&arguments.session_binding)
|
||||
.map_err(|error| context("session binding", error))?;
|
||||
let sandbox_key_bytes: [u8; 32] = bundle
|
||||
.sandbox_private_key
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|error| context("sandbox signing key", error))?;
|
||||
let sandbox_key = SigningKey::from_bytes(&sandbox_key_bytes);
|
||||
let session = VerifiedSession::new(
|
||||
publication.trust_manifest_sha256,
|
||||
|
|
@ -52,16 +65,29 @@ fn run() -> Result<u8, Box<dyn std::error::Error>> {
|
|||
bundle.coordinator_protocol_credential(),
|
||||
bundle.sandbox_protocol_credential(),
|
||||
registration.session_capability,
|
||||
)?;
|
||||
)
|
||||
.map_err(|error| context("session authority", error))?;
|
||||
let client = EscalationClient::new(
|
||||
RegularFileMailbox::open(&arguments.inbox)?,
|
||||
RegularFileMailbox::open(&arguments.outbox)?,
|
||||
RegularFileMailbox::open(&arguments.inbox)
|
||||
.map_err(|error| context("request mailbox", error))?,
|
||||
RegularFileMailbox::open(&arguments.outbox)
|
||||
.map_err(|error| context("result mailbox", error))?,
|
||||
session,
|
||||
sandbox_key,
|
||||
);
|
||||
let mut request = client.request(arguments.command_argv, arguments.justification)?;
|
||||
let mut cancellation =
|
||||
CancellationSignals::register().map_err(|error| context("cancellation signals", error))?;
|
||||
let mut request = client
|
||||
.request(arguments.command_argv, arguments.justification)
|
||||
.map_err(|error| context("request publication", error))?;
|
||||
loop {
|
||||
match request.poll()? {
|
||||
if cancellation.requested()? {
|
||||
request.cancel()?;
|
||||
}
|
||||
match request
|
||||
.poll()
|
||||
.map_err(|error| context("result collection", error))?
|
||||
{
|
||||
ClientEvent::Output { stream, bytes, .. } => match stream {
|
||||
lazaret_core::records::ResultStream::Stdout => {
|
||||
io::stdout().write_all(&bytes)?;
|
||||
|
|
@ -89,6 +115,58 @@ fn run() -> Result<u8, Box<dyn std::error::Error>> {
|
|||
}
|
||||
}
|
||||
|
||||
fn context(label: &str, error: impl std::fmt::Display) -> io::Error {
|
||||
io::Error::other(format!("{label}: {error}"))
|
||||
}
|
||||
|
||||
struct CancellationSignals {
|
||||
descriptor: SignalFd,
|
||||
previous: SigSet,
|
||||
published: bool,
|
||||
}
|
||||
|
||||
impl CancellationSignals {
|
||||
fn register() -> io::Result<Self> {
|
||||
let mut signals = SigSet::empty();
|
||||
signals.add(Signal::SIGINT);
|
||||
signals.add(Signal::SIGTERM);
|
||||
let previous = signals
|
||||
.thread_swap_mask(SigmaskHow::SIG_BLOCK)
|
||||
.map_err(io::Error::from)?;
|
||||
let descriptor =
|
||||
SignalFd::with_flags(&signals, SfdFlags::SFD_CLOEXEC | SfdFlags::SFD_NONBLOCK)
|
||||
.map_err(io::Error::from)?;
|
||||
Ok(Self {
|
||||
descriptor,
|
||||
previous,
|
||||
published: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn requested(&mut self) -> io::Result<bool> {
|
||||
if self.published {
|
||||
return Ok(false);
|
||||
}
|
||||
if self
|
||||
.descriptor
|
||||
.read_signal()
|
||||
.map_err(io::Error::from)?
|
||||
.is_some()
|
||||
{
|
||||
self.published = true;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CancellationSignals {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.previous.thread_set_mask();
|
||||
}
|
||||
}
|
||||
|
||||
fn read_record<T>(path: &std::path::Path) -> Result<T, Box<dyn std::error::Error>>
|
||||
where
|
||||
T: serde::de::DeserializeOwned + serde::Serialize,
|
||||
|
|
@ -117,7 +195,10 @@ where
|
|||
Ok(decode(&bytes)?)
|
||||
}
|
||||
|
||||
fn parse_arguments(mut arguments: Vec<OsString>) -> Result<Arguments, io::Error> {
|
||||
fn parse_arguments_with_session(
|
||||
mut arguments: Vec<OsString>,
|
||||
session: Option<OsString>,
|
||||
) -> Result<Arguments, io::Error> {
|
||||
let separator = arguments
|
||||
.iter()
|
||||
.position(|argument| argument == "--")
|
||||
|
|
@ -156,6 +237,28 @@ fn parse_arguments(mut arguments: Vec<OsString>) -> Result<Arguments, io::Error>
|
|||
_ => return Err(io::Error::other("unknown client option")),
|
||||
}
|
||||
}
|
||||
if let Some(session) = session {
|
||||
let session = session
|
||||
.into_string()
|
||||
.map_err(|_| io::Error::other("LAZARET_SESSION_ID is not UTF-8"))?;
|
||||
if session.len() != 64
|
||||
|| !session
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
|
||||
{
|
||||
return Err(io::Error::other("LAZARET_SESSION_ID is invalid"));
|
||||
}
|
||||
let credentials = PathBuf::from("/run/lazaret/credentials");
|
||||
inbox.get_or_insert_with(|| PathBuf::from("/run/lazaret/mailbox/requests"));
|
||||
outbox.get_or_insert_with(|| PathBuf::from("/run/lazaret/mailbox/results"));
|
||||
publication.get_or_insert_with(|| credentials.join("publication.postcard"));
|
||||
sandbox_credentials.get_or_insert_with(|| credentials.join("sandbox-credentials.postcard"));
|
||||
registration.get_or_insert_with(|| {
|
||||
credentials.join(format!("session-{session}-registration.postcard"))
|
||||
});
|
||||
session_binding
|
||||
.get_or_insert_with(|| credentials.join(format!("session-{session}-binding.postcard")));
|
||||
}
|
||||
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"))?,
|
||||
|
|
@ -173,7 +276,7 @@ fn parse_arguments(mut arguments: Vec<OsString>) -> Result<Arguments, io::Error>
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_arguments;
|
||||
use super::parse_arguments_with_session;
|
||||
use std::ffi::OsString;
|
||||
|
||||
fn valid_arguments() -> Vec<OsString> {
|
||||
|
|
@ -204,7 +307,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parser_preserves_exact_argv_without_shell_interpretation() {
|
||||
let parsed = parse_arguments(valid_arguments()).unwrap();
|
||||
let parsed = parse_arguments_with_session(valid_arguments(), None).unwrap();
|
||||
assert_eq!(
|
||||
parsed.command_argv,
|
||||
[
|
||||
|
|
@ -219,14 +322,48 @@ mod tests {
|
|||
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());
|
||||
assert!(parse_arguments_with_session(missing_separator, None).is_err());
|
||||
|
||||
let mut unknown = valid_arguments();
|
||||
unknown[0] = OsString::from("--unknown");
|
||||
assert!(parse_arguments(unknown).is_err());
|
||||
assert!(parse_arguments_with_session(unknown, None).is_err());
|
||||
|
||||
let mut empty = valid_arguments();
|
||||
empty.truncate(empty.iter().position(|argument| argument == "--").unwrap() + 1);
|
||||
assert!(parse_arguments(empty).is_err());
|
||||
assert!(parse_arguments_with_session(empty, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_environment_supplies_only_fixed_generation_mount_paths() {
|
||||
let arguments = ["--justification", "needed", "--", "/usr/bin/tool"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect();
|
||||
let session = "11".repeat(32);
|
||||
let parsed =
|
||||
parse_arguments_with_session(arguments, Some(OsString::from(&session))).unwrap();
|
||||
assert_eq!(
|
||||
parsed.inbox,
|
||||
std::path::Path::new("/run/lazaret/mailbox/requests")
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.outbox,
|
||||
std::path::Path::new("/run/lazaret/mailbox/results")
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.registration,
|
||||
std::path::Path::new(&format!(
|
||||
"/run/lazaret/credentials/session-{session}-registration.postcard"
|
||||
))
|
||||
);
|
||||
|
||||
let arguments = ["--justification", "needed", "--", "/usr/bin/tool"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect();
|
||||
assert!(
|
||||
parse_arguments_with_session(arguments, Some(OsString::from("not-an-identity")))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue