898 lines
33 KiB
Rust
898 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, 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(outcome) = lazaret_host_coordinator::private_roles::REGISTRY.dispatch(&arguments) {
|
|
if let Err(error) = outcome.result {
|
|
eprintln!("private role {} failed: {error}", outcome.name);
|
|
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);
|
|
}
|
|
}
|