lazaret/crates/lazaret-host-coordinator/tests/escalation.rs

900 lines
33 KiB
Rust

use std::error::Error;
use std::fs::{File, OpenOptions};
use std::os::fd::AsFd;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::fs::{MetadataExt, symlink};
use std::path::PathBuf;
use std::time::{Duration, Instant};
use ed25519_dalek::SigningKey;
use lazaret_core::identity::{
ClientId, ConfigurationFingerprint, CoordinatorId, DomainId, GenerationId, RequestId,
SandboxId, SessionId, StreamDigest,
};
use lazaret_core::protocol::{
MAX_SIGNED_RECORD_BYTES, ProtocolValidationError, VerifiedSession, acknowledgment_file_name,
encode_signed, issue_session_capability, request_file_name,
};
use lazaret_core::records::{
DeveloperDecision, DeveloperDecisionKind, EscalationOutcome, GenerationIdentity,
HostProcessOwnershipSlot, MailboxCancellation, MailboxResult, ProcessOwnershipCommitAck,
ProtocolVersion, SessionBinding, StagedProcessOwnership,
};
use lazaret_escalation_client::{ClientEvent, ClientRequest, EscalationClient};
use lazaret_host_coordinator::control::TrustAnchor;
use lazaret_host_coordinator::escalation::{
ApprovalSource, DeveloperExecutableResolver, DirectDeveloperExecutableResolver,
EscalationCoordinator, EscalationError, EscalationPhase, ExecutionOwnershipRecorder,
GenerationMailbox,
};
use lazaret_linux::filesystem::RegularFileMailbox;
use lazaret_linux::process::{
ProcessOwnershipContext, dispatch_private_process_role, recover_process,
};
use sha2::{Digest, Sha256};
type TestResult<T = ()> = Result<T, Box<dyn Error>>;
struct TestDirectory(PathBuf);
impl TestDirectory {
fn create(name: &str) -> TestResult<Self> {
let path = std::env::temp_dir().join(format!(
"lazaret-escalation-{name}-{}-{}",
std::process::id(),
Instant::now().elapsed().as_nanos()
));
std::fs::create_dir(&path)?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700))?;
Ok(Self(path))
}
fn child(&self, name: &str) -> TestResult<PathBuf> {
let path = self.0.join(name);
std::fs::create_dir(&path)?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700))?;
Ok(path)
}
}
impl Drop for TestDirectory {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
struct Fixture {
session: VerifiedSession,
sandbox_key: SigningKey,
coordinator_key: SigningKey,
anchor_key: SigningKey,
generation: GenerationIdentity,
}
fn repeated(value: u8) -> [u8; 32] {
[value; 32]
}
fn make_fixture(seed: u8) -> TestResult<Fixture> {
let generation = GenerationIdentity {
agent_profile: format!("profile-{seed}"),
domain_id: DomainId::new(repeated(seed)),
generation_id: GenerationId::new(u64::from(seed) + 1),
configuration_fingerprint: ConfigurationFingerprint::new(repeated(seed.wrapping_add(1))),
coordinator_id: CoordinatorId::new(repeated(seed.wrapping_add(2))),
sandbox_id: SandboxId::new(repeated(seed.wrapping_add(3))),
};
let mut anchor = TrustAnchor::create(generation.clone())?;
let publication = anchor.publication_identity();
let coordinator_key = anchor.coordinator_protocol_key();
let bundle = anchor.take_sandbox_credential_bundle()?;
let sandbox_bytes: [u8; 32] = bundle.sandbox_private_key.as_slice().try_into()?;
let sandbox_key = SigningKey::from_bytes(&sandbox_bytes);
let anchor_key = SigningKey::from_bytes(&repeated(seed.wrapping_add(4)));
let binding = SessionBinding {
generation_identity: generation.clone(),
cli_id: ClientId::new(repeated(seed.wrapping_add(5))),
session_id: SessionId::new(repeated(seed.wrapping_add(6))),
session_anchor_public_key: anchor_key.verifying_key().to_bytes().to_vec(),
};
let capability = issue_session_capability(
binding.clone(),
bundle.coordinator_protocol_credential().credential_id,
bundle.sandbox_protocol_credential().credential_id,
&coordinator_key,
)?;
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(),
capability,
)?;
Ok(Fixture {
session,
sandbox_key,
coordinator_key,
anchor_key,
generation,
})
}
struct Mailboxes {
root: TestDirectory,
inbox_path: PathBuf,
outbox_path: PathBuf,
}
impl Mailboxes {
fn create(name: &str) -> TestResult<Self> {
let root = TestDirectory::create(name)?;
let inbox_path = root.child("inbox")?;
let outbox_path = root.child("outbox")?;
Ok(Self {
root,
inbox_path,
outbox_path,
})
}
fn client(&self, fixture: &Fixture) -> TestResult<EscalationClient> {
Ok(EscalationClient::new(
RegularFileMailbox::open(&self.inbox_path)?,
RegularFileMailbox::open(&self.outbox_path)?,
fixture.session.clone(),
fixture.sandbox_key.clone(),
))
}
fn coordinator(
&self,
fixture: &Fixture,
request_timeout: Duration,
acknowledgment_timeout: Duration,
) -> TestResult<EscalationCoordinator> {
Ok(EscalationCoordinator::new(
fixture.coordinator_key.clone(),
RegularFileMailbox::open(&self.outbox_path)?,
ProcessOwnershipContext {
domain_id: fixture.generation.domain_id,
generation_id: fixture.generation.generation_id,
coordinator_id: fixture.generation.coordinator_id,
slot: HostProcessOwnershipSlot::EscalationHostProcess(RequestId::new([0; 32])),
},
rustix::process::getuid().as_raw(),
request_timeout,
acknowledgment_timeout,
))
}
fn consume_request(&self, request_id: RequestId) -> TestResult<Vec<u8>> {
Ok(RegularFileMailbox::open(&self.inbox_path)?
.consume(&request_file_name(request_id), MAX_SIGNED_RECORD_BYTES)?)
}
fn consume_ack(&self, request_id: RequestId, sequence: u64) -> TestResult<Vec<u8>> {
Ok(RegularFileMailbox::open(&self.inbox_path)?.consume(
&acknowledgment_file_name(request_id, sequence),
MAX_SIGNED_RECORD_BYTES,
)?)
}
fn result_chunk_count(&self) -> TestResult<usize> {
Ok(RegularFileMailbox::open(&self.outbox_path)?
.names()?
.iter()
.filter(|name| name.as_encoded_bytes().starts_with(b"result-chunk-"))
.count())
}
fn command(&self, source: &str, name: &str) -> TestResult<Vec<u8>> {
let destination = self.root.0.join(name);
std::fs::copy(source, &destination)?;
std::fs::set_permissions(&destination, std::fs::Permissions::from_mode(0o700))?;
Ok(destination.as_os_str().as_encoded_bytes().to_vec())
}
}
#[derive(Default)]
struct RecordingOwnership {
staged: Option<StagedProcessOwnership>,
last: Option<StagedProcessOwnership>,
reject_stage: bool,
}
impl ExecutionOwnershipRecorder for RecordingOwnership {
fn stage(&mut self, ownership: StagedProcessOwnership) -> std::io::Result<()> {
self.last = Some(ownership);
if self.reject_stage {
return Err(std::io::Error::other("injected ownership refusal"));
}
self.staged = Some(ownership);
Ok(())
}
fn acknowledge(
&mut self,
ownership: StagedProcessOwnership,
) -> std::io::Result<ProcessOwnershipCommitAck> {
if self.staged.take() == Some(ownership) {
Ok(ProcessOwnershipCommitAck::v1(ownership))
} else {
Err(std::io::Error::other("staged ownership mismatch"))
}
}
}
fn developer_decision(
fixture: &Fixture,
request: &ClientRequest<'_>,
decision: DeveloperDecisionKind,
argv: Vec<Vec<u8>>,
) -> TestResult<Vec<u8>> {
Ok(encode_signed(
&DeveloperDecision {
protocol_version: ProtocolVersion::V1,
session_binding: fixture.session.session_binding().clone(),
request_id: request.request_id(),
request_sha256: request.request_digest(),
decision,
approved_command_argv: argv,
},
&fixture.anchor_key,
)?)
}
fn executable(argv: &[Vec<u8>]) -> TestResult<std::os::fd::OwnedFd> {
Ok(DirectDeveloperExecutableResolver::open()?.resolve(argv)?)
}
fn drive_terminal(
mailboxes: &Mailboxes,
coordinator: &mut EscalationCoordinator,
request: &mut ClientRequest<'_>,
) -> TestResult<(EscalationOutcome, Vec<u8>)> {
let mut output = Vec::new();
for _ in 0..5_000 {
let _ = coordinator.progress(request.request_id(), Instant::now())?;
match request.poll()? {
ClientEvent::Output {
sequence, bytes, ..
} => {
output.extend_from_slice(&bytes);
let ack = mailboxes.consume_ack(request.request_id(), sequence)?;
coordinator.acknowledge(request.request_id(), &ack, Instant::now())?;
}
ClientEvent::Terminal(result) => return Ok((result.outcome, output)),
ClientEvent::Pending => std::thread::sleep(Duration::from_millis(1)),
}
}
Err("terminal result deadline exhausted".into())
}
fn acknowledge_one_chunk(
mailboxes: &Mailboxes,
coordinator: &mut EscalationCoordinator,
request: &mut ClientRequest<'_>,
now: Instant,
) -> TestResult<Vec<u8>> {
for _ in 0..500 {
coordinator.progress(request.request_id(), now)?;
match request.poll()? {
ClientEvent::Output {
sequence, bytes, ..
} => {
let ack = mailboxes.consume_ack(request.request_id(), sequence)?;
coordinator.acknowledge(request.request_id(), &ack, now)?;
return Ok(bytes);
}
ClientEvent::Terminal(_) => {
return Err("request completed before partial output".into());
}
ClientEvent::Pending => std::thread::sleep(Duration::from_millis(1)),
}
}
Err("partial output deadline exhausted".into())
}
fn completed_auto_and_developer_requests_preserve_attribution() -> TestResult {
for (seed, auto) in [(10, true), (30, false)] {
let fixture = make_fixture(seed)?;
let mailboxes = Mailboxes::create(if auto { "auto" } else { "developer" })?;
let client = mailboxes.client(&fixture)?;
let argv = vec![
mailboxes.command("/usr/bin/printf", "printf")?,
b"hello".to_vec(),
];
let mut request = client.request(argv.clone(), "synthetic output".into())?;
let signed_request = mailboxes.consume_request(request.request_id())?;
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_secs(5), Duration::from_secs(2))?;
let mut ownership = RecordingOwnership::default();
if auto {
assert_eq!(
coordinator.receive_auto_approved(
fixture.session.clone(),
&signed_request,
executable(&argv)?,
argv,
&mut ownership,
Instant::now(),
)?,
EscalationPhase::Executing
);
assert_eq!(
coordinator.approval_source(request.request_id()),
Some(ApprovalSource::AutoRule)
);
} else {
coordinator.receive_for_developer_approval(
fixture.session.clone(),
&signed_request,
Instant::now(),
)?;
let decision =
developer_decision(&fixture, &request, DeveloperDecisionKind::Approve, argv)?;
let mut resolver = DirectDeveloperExecutableResolver::open()?;
coordinator.apply_developer_decision(
request.request_id(),
&decision,
&mut resolver,
&mut ownership,
)?;
assert_eq!(
coordinator.approval_source(request.request_id()),
Some(ApprovalSource::Developer)
);
}
let (outcome, output) = drive_terminal(&mailboxes, &mut coordinator, &mut request)?;
assert_eq!(outcome, EscalationOutcome::Completed);
assert_eq!(output, b"hello");
assert_eq!(
coordinator
.approval_attribution(request.request_id())
.unwrap()
.source,
if auto {
ApprovalSource::AutoRule
} else {
ApprovalSource::Developer
}
);
assert!(
RegularFileMailbox::open(&mailboxes.outbox_path)?
.names()?
.contains(&lazaret_core::protocol::result_file_name(
request.request_id()
))
);
coordinator.session_lost(fixture.session.session_binding().session_id)?;
assert!(
RegularFileMailbox::open(&mailboxes.outbox_path)?
.names()?
.is_empty()
);
assert!(recover_process(ownership.last.unwrap().identity()).is_err());
}
Ok(())
}
fn denial_failure_and_request_timeout_publish_terminal_outcomes() -> TestResult {
let fixture = make_fixture(50)?;
let mailboxes = Mailboxes::create("terminal-outcomes")?;
let client = mailboxes.client(&fixture)?;
let argv = vec![
mailboxes.command("/usr/bin/printf", "printf")?,
b"denied".to_vec(),
];
let mut denied = client.request(argv, "deny".into())?;
let signed = mailboxes.consume_request(denied.request_id())?;
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_secs(5), Duration::from_secs(2))?;
coordinator.receive_for_developer_approval(fixture.session.clone(), &signed, Instant::now())?;
let decision = developer_decision(&fixture, &denied, DeveloperDecisionKind::Deny, Vec::new())?;
let mut resolver = DirectDeveloperExecutableResolver::open()?;
coordinator.apply_developer_decision(
denied.request_id(),
&decision,
&mut resolver,
&mut RecordingOwnership::default(),
)?;
assert!(
matches!(denied.poll()?, ClientEvent::Terminal(result) if result.outcome == EscalationOutcome::Denied)
);
let fixture = make_fixture(60)?;
let mailboxes = Mailboxes::create("failed")?;
let client = mailboxes.client(&fixture)?;
let argv = vec![
mailboxes.command("/usr/bin/printf", "printf")?,
b"failed".to_vec(),
];
let mut failed = client.request(argv.clone(), "failure".into())?;
let signed = mailboxes.consume_request(failed.request_id())?;
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_secs(5), Duration::from_secs(2))?;
let mut ownership = RecordingOwnership {
reject_stage: true,
..RecordingOwnership::default()
};
assert_eq!(
coordinator.receive_auto_approved(
fixture.session.clone(),
&signed,
executable(&argv)?,
argv,
&mut ownership,
Instant::now(),
)?,
EscalationPhase::Delivered(EscalationOutcome::Failed)
);
assert!(
matches!(failed.poll()?, ClientEvent::Terminal(result) if result.outcome == EscalationOutcome::Failed)
);
assert!(recover_process(ownership.last.unwrap().identity()).is_err());
let fixture = make_fixture(70)?;
let mailboxes = Mailboxes::create("request-timeout")?;
let client = mailboxes.client(&fixture)?;
let mut timed_out = client.request(
vec![mailboxes.command("/usr/bin/printf", "printf")?],
"timeout".into(),
)?;
let signed = mailboxes.consume_request(timed_out.request_id())?;
let start = Instant::now();
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_millis(10), Duration::from_secs(2))?;
coordinator.receive_for_developer_approval(fixture.session.clone(), &signed, start)?;
assert_eq!(
coordinator.expire(start + Duration::from_millis(20))?,
vec![timed_out.request_id()]
);
assert!(
matches!(timed_out.poll()?, ClientEvent::Terminal(result) if result.outcome == EscalationOutcome::TimedOut)
);
Ok(())
}
fn cancellation_session_loss_and_ack_timeout_reap_exact_children() -> TestResult {
let fixture = make_fixture(80)?;
let mailboxes = Mailboxes::create("cancel")?;
let client = mailboxes.client(&fixture)?;
let argv = vec![
mailboxes.command("/usr/bin/sleep", "sleep")?,
b"30".to_vec(),
];
let mut request = client.request(argv.clone(), "cancel".into())?;
let signed = mailboxes.consume_request(request.request_id())?;
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_secs(5), Duration::from_secs(2))?;
let mut ownership = RecordingOwnership::default();
coordinator.receive_auto_approved(
fixture.session.clone(),
&signed,
executable(&argv)?,
argv,
&mut ownership,
Instant::now(),
)?;
request.cancel()?;
let cancellation_name = lazaret_core::protocol::cancellation_file_name(request.request_id());
let cancellation = RegularFileMailbox::open(&mailboxes.inbox_path)?
.consume(&cancellation_name, MAX_SIGNED_RECORD_BYTES)?;
coordinator.cancel(request.request_id(), &cancellation)?;
assert!(
matches!(request.poll()?, ClientEvent::Terminal(result) if result.outcome == EscalationOutcome::Cancelled)
);
assert!(recover_process(ownership.last.unwrap().identity()).is_err());
let fixture = make_fixture(90)?;
let mailboxes = Mailboxes::create("session-loss")?;
let client = mailboxes.client(&fixture)?;
let argv = vec![
mailboxes.command("/usr/bin/sleep", "sleep")?,
b"30".to_vec(),
];
let request = client.request(argv.clone(), "session loss".into())?;
let signed = mailboxes.consume_request(request.request_id())?;
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_secs(5), Duration::from_secs(2))?;
let mut ownership = RecordingOwnership::default();
coordinator.receive_auto_approved(
fixture.session.clone(),
&signed,
executable(&argv)?,
argv,
&mut ownership,
Instant::now(),
)?;
coordinator.session_lost(fixture.session.session_binding().session_id)?;
assert!(recover_process(ownership.last.unwrap().identity()).is_err());
assert!(
RegularFileMailbox::open(&mailboxes.outbox_path)?
.names()?
.is_empty()
);
let fixture = make_fixture(100)?;
let mailboxes = Mailboxes::create("ack-timeout")?;
let client = mailboxes.client(&fixture)?;
let argv = vec![
mailboxes.command("/usr/bin/head", "head")?,
b"-c".to_vec(),
b"1000000".to_vec(),
b"/dev/zero".to_vec(),
];
let mut request = client.request(argv.clone(), "backpressure timeout".into())?;
let signed = mailboxes.consume_request(request.request_id())?;
let start = Instant::now();
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_secs(5), Duration::from_millis(10))?;
let mut ownership = RecordingOwnership::default();
coordinator.receive_auto_approved(
fixture.session.clone(),
&signed,
executable(&argv)?,
argv,
&mut ownership,
start,
)?;
for _ in 0..500 {
coordinator.progress(request.request_id(), start)?;
if mailboxes.result_chunk_count()? > 0 {
break;
}
std::thread::sleep(Duration::from_millis(1));
}
assert!(mailboxes.result_chunk_count()? > 0);
coordinator.expire(start + Duration::from_millis(20))?;
assert!(
matches!(request.poll()?, ClientEvent::Terminal(result) if result.outcome == EscalationOutcome::TimedOut)
);
assert!(recover_process(ownership.last.unwrap().identity()).is_err());
Ok(())
}
fn output_window_never_exceeds_eight_unacknowledged_chunks() -> TestResult {
let fixture = make_fixture(110)?;
let mailboxes = Mailboxes::create("window")?;
let client = mailboxes.client(&fixture)?;
let argv = vec![
mailboxes.command("/usr/bin/head", "head")?,
b"-c".to_vec(),
b"1000000".to_vec(),
b"/dev/zero".to_vec(),
];
let mut request = client.request(argv.clone(), "bounded output".into())?;
let signed = mailboxes.consume_request(request.request_id())?;
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_secs(10), Duration::from_secs(5))?;
coordinator.receive_auto_approved(
fixture.session.clone(),
&signed,
executable(&argv)?,
argv,
&mut RecordingOwnership::default(),
Instant::now(),
)?;
let mut reached_bound = false;
for _ in 0..1_000 {
coordinator.progress(request.request_id(), Instant::now())?;
let count = mailboxes.result_chunk_count()?;
assert!(count <= 8);
if count == 8 {
reached_bound = true;
break;
}
std::thread::sleep(Duration::from_millis(1));
}
assert!(reached_bound);
let (outcome, output) = drive_terminal(&mailboxes, &mut coordinator, &mut request)?;
assert_eq!(outcome, EscalationOutcome::Completed);
assert_eq!(output.len(), 1_000_000);
Ok(())
}
fn interrupted_execution_commits_acknowledged_output() -> TestResult {
let fixture = make_fixture(115)?;
let mailboxes = Mailboxes::create("cancel-after-output")?;
let client = mailboxes.client(&fixture)?;
let argv = vec![
mailboxes.command("/usr/bin/head", "head")?,
b"-c".to_vec(),
b"1000000".to_vec(),
b"/dev/zero".to_vec(),
];
let mut request = client.request(argv.clone(), "cancel after output".into())?;
let signed = mailboxes.consume_request(request.request_id())?;
let now = Instant::now();
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_secs(5), Duration::from_secs(2))?;
coordinator.receive_auto_approved(
fixture.session.clone(),
&signed,
executable(&argv)?,
argv,
&mut RecordingOwnership::default(),
now,
)?;
assert!(!acknowledge_one_chunk(&mailboxes, &mut coordinator, &mut request, now)?.is_empty());
request.cancel()?;
let cancellation = RegularFileMailbox::open(&mailboxes.inbox_path)?.consume(
&lazaret_core::protocol::cancellation_file_name(request.request_id()),
MAX_SIGNED_RECORD_BYTES,
)?;
coordinator.cancel(request.request_id(), &cancellation)?;
assert!(
matches!(request.poll()?, ClientEvent::Terminal(result) if result.outcome == EscalationOutcome::Cancelled)
);
let fixture = make_fixture(116)?;
let mailboxes = Mailboxes::create("timeout-after-output")?;
let client = mailboxes.client(&fixture)?;
let argv = vec![
mailboxes.command("/usr/bin/head", "head")?,
b"-c".to_vec(),
b"1000000".to_vec(),
b"/dev/zero".to_vec(),
];
let mut request = client.request(argv.clone(), "timeout after output".into())?;
let signed = mailboxes.consume_request(request.request_id())?;
let start = Instant::now();
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_millis(10), Duration::from_secs(2))?;
coordinator.receive_auto_approved(
fixture.session.clone(),
&signed,
executable(&argv)?,
argv,
&mut RecordingOwnership::default(),
start,
)?;
assert!(!acknowledge_one_chunk(&mailboxes, &mut coordinator, &mut request, start)?.is_empty());
assert_eq!(
coordinator.expire(start + Duration::from_millis(20))?,
vec![request.request_id()]
);
assert!(
matches!(request.poll()?, ClientEvent::Terminal(result) if result.outcome == EscalationOutcome::TimedOut)
);
Ok(())
}
fn replay_and_session_tuple_mismatch_refuse() -> TestResult {
let fixture = make_fixture(120)?;
let mailboxes = Mailboxes::create("replay")?;
let client = mailboxes.client(&fixture)?;
let request = client.request(
vec![mailboxes.command("/usr/bin/printf", "printf")?],
"replay".into(),
)?;
let signed = mailboxes.consume_request(request.request_id())?;
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_secs(5), Duration::from_secs(2))?;
coordinator.receive_for_developer_approval(fixture.session.clone(), &signed, Instant::now())?;
coordinator.session_lost(fixture.session.session_binding().session_id)?;
assert!(matches!(
coordinator.receive_for_developer_approval(
fixture.session.clone(),
&signed,
Instant::now()
),
Err(EscalationError::Protocol(
ProtocolValidationError::RequestReplay
))
));
let other = make_fixture(130)?;
let other_mailboxes = Mailboxes::create("tuple")?;
let mut other_coordinator =
other_mailboxes.coordinator(&other, Duration::from_secs(5), Duration::from_secs(2))?;
assert!(matches!(
other_coordinator.receive_for_developer_approval(other.session, &signed, Instant::now()),
Err(EscalationError::Protocol(
ProtocolValidationError::SessionBindingMismatch
| ProtocolValidationError::InvalidPublicKey
| ProtocolValidationError::Codec(_)
))
));
Ok(())
}
fn signed_cancellation_decision_and_result_tuple_mismatches_refuse() -> TestResult {
let fixture = make_fixture(150)?;
let other = make_fixture(160)?;
let mailboxes = Mailboxes::create("signed-tuples")?;
let client = mailboxes.client(&fixture)?;
let mut request = client.request(
vec![mailboxes.command("/usr/bin/printf", "printf")?],
"tuple mismatch".into(),
)?;
let signed = mailboxes.consume_request(request.request_id())?;
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_secs(5), Duration::from_secs(2))?;
coordinator.receive_for_developer_approval(fixture.session.clone(), &signed, Instant::now())?;
let cancellation = encode_signed(
&MailboxCancellation {
protocol_version: ProtocolVersion::V1,
session_binding: other.session.session_binding().clone(),
request_id: request.request_id(),
session_capability: other.session.session_capability().clone(),
sandbox_credential_id: other.session.sandbox_credential_id(),
},
&other.sandbox_key,
)?;
assert!(matches!(
coordinator.cancel(request.request_id(), &cancellation),
Err(EscalationError::Protocol(_))
));
let wrong_decision = encode_signed(
&DeveloperDecision {
protocol_version: ProtocolVersion::V1,
session_binding: other.session.session_binding().clone(),
request_id: request.request_id(),
request_sha256: request.request_digest(),
decision: DeveloperDecisionKind::Deny,
approved_command_argv: Vec::new(),
},
&fixture.anchor_key,
)?;
assert!(matches!(
coordinator.apply_developer_decision(
request.request_id(),
&wrong_decision,
&mut DirectDeveloperExecutableResolver::open()?,
&mut RecordingOwnership::default(),
),
Err(EscalationError::Protocol(
ProtocolValidationError::SessionBindingMismatch
))
));
let empty = StreamDigest::new(Sha256::digest([]).into());
let wrong_result = encode_signed(
&MailboxResult {
protocol_version: ProtocolVersion::V1,
session_binding: other.session.session_binding().clone(),
request_id: request.request_id(),
request_sha256: request.request_digest(),
coordinator_credential_id: fixture.session.coordinator_credential_id(),
outcome: EscalationOutcome::Denied,
exit_code: None,
chunk_count: 0,
stdout_sha256: empty,
stderr_sha256: empty,
},
&fixture.coordinator_key,
)?;
RegularFileMailbox::open(&mailboxes.outbox_path)?.publish(
&lazaret_core::protocol::result_file_name(request.request_id()),
&wrong_result,
)?;
assert!(matches!(
request.poll(),
Err(lazaret_escalation_client::ClientError::Protocol(
ProtocolValidationError::SessionBindingMismatch
))
));
coordinator.session_lost(fixture.session.session_binding().session_id)?;
Ok(())
}
fn exact_descriptor_launcher_survives_path_replacement() -> TestResult {
let fixture = make_fixture(140)?;
let mailboxes = Mailboxes::create("descriptor")?;
let client = mailboxes.client(&fixture)?;
let root = TestDirectory::create("descriptor-target")?;
let executable_path = root.0.join("command");
std::fs::copy("/usr/bin/printf", &executable_path)?;
std::fs::set_permissions(&executable_path, std::fs::Permissions::from_mode(0o700))?;
let argv = vec![
executable_path.as_os_str().as_encoded_bytes().to_vec(),
b"descriptor".to_vec(),
];
let mut request = client.request(argv.clone(), "descriptor authority".into())?;
let signed = mailboxes.consume_request(request.request_id())?;
let executable = OpenOptions::new().read(true).open(&executable_path)?;
let executable = executable.into();
std::fs::rename(&executable_path, root.0.join("approved"))?;
std::fs::write(&executable_path, b"not executable")?;
let mut coordinator =
mailboxes.coordinator(&fixture, Duration::from_secs(5), Duration::from_secs(2))?;
coordinator.receive_auto_approved(
fixture.session.clone(),
&signed,
executable,
argv,
&mut RecordingOwnership::default(),
Instant::now(),
)?;
let (outcome, output) = drive_terminal(&mailboxes, &mut coordinator, &mut request)?;
assert_eq!(outcome, EscalationOutcome::Completed);
assert_eq!(output, b"descriptor");
Ok(())
}
fn generation_mailbox_is_confined_and_generation_scoped() -> TestResult {
let fixture = make_fixture(170)?;
let root = TestDirectory::create("generation-mailbox")?;
let root_file = File::open(&root.0)?;
let mailbox = GenerationMailbox::create(
root_file.as_fd(),
&fixture.generation,
rustix::process::getuid().as_raw(),
)?;
assert!(mailbox.inbox().names()?.is_empty());
assert!(mailbox.outbox().names()?.is_empty());
let generation_name =
lazaret_core::protocol::mailbox_generation_name(fixture.generation.generation_id.get());
let generation_path = root.0.join("mailbox").join(generation_name);
let identity = std::fs::symlink_metadata(generation_path.join("generation-identity.postcard"))?;
assert!(identity.file_type().is_file());
assert_eq!(identity.nlink(), 1);
assert_eq!(identity.mode() & 0o7777, 0o600);
for directory in [
generation_path.join("inbox"),
generation_path.join("outbox"),
] {
let metadata = std::fs::symlink_metadata(directory)?;
assert!(metadata.file_type().is_dir());
assert_eq!(metadata.mode() & 0o7777, 0o700);
}
assert!(
GenerationMailbox::create(
root_file.as_fd(),
&fixture.generation,
rustix::process::getuid().as_raw(),
)
.is_err()
);
let hostile = TestDirectory::create("generation-mailbox-link")?;
symlink(&root.0, hostile.0.join("mailbox"))?;
let hostile_file = File::open(&hostile.0)?;
assert!(
GenerationMailbox::create(
hostile_file.as_fd(),
&fixture.generation,
rustix::process::getuid().as_raw(),
)
.is_err()
);
Ok(())
}
fn run() -> TestResult {
completed_auto_and_developer_requests_preserve_attribution()?;
denial_failure_and_request_timeout_publish_terminal_outcomes()?;
cancellation_session_loss_and_ack_timeout_reap_exact_children()?;
output_window_never_exceeds_eight_unacknowledged_chunks()?;
interrupted_execution_commits_acknowledged_output()?;
replay_and_session_tuple_mismatch_refuse()?;
signed_cancellation_decision_and_result_tuple_mismatches_refuse()?;
exact_descriptor_launcher_survives_path_replacement()?;
generation_mailbox_is_confined_and_generation_scoped()?;
Ok(())
}
fn main() {
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
if let Some(result) = dispatch_private_process_role(&arguments) {
if let Err(error) = result {
eprintln!("private process role failed: {error}");
std::process::exit(1);
}
return;
}
if arguments.iter().any(|argument| argument == "--list") {
println!("escalation: test");
return;
}
if let Err(error) = run() {
eprintln!("escalation verifier failed: {error}");
std::process::exit(1);
}
}