#[cfg(unix)]
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use super::evidence::ExecutionIdentity;
pub const ENV_CONTROL_DOWNLINK: &str = "VETTO_VNG_CONTROL_DOWNLINK";
pub const ENV_CONTROL_UPLINK: &str = "VETTO_VNG_CONTROL_UPLINK";
pub const CHALLENGE_FIFO: &str = "challenge.fifo";
pub const RESPONSE_FIFO: &str = "response.fifo";
pub const MAX_CONTROL_BYTES: usize = 256;
pub const CONTROL_READ_BUDGET: Duration = Duration::from_secs(2);
#[cfg(unix)]
pub struct ControlChannel {
dir: PathBuf,
downlink: PathBuf,
uplink: PathBuf,
expected: String,
uplink_reader: std::os::fd::OwnedFd,
_downlink_writer: std::os::fd::OwnedFd,
}
#[cfg(unix)]
impl ControlChannel {
pub fn create(identity: &ExecutionIdentity) -> std::io::Result<Self> {
let challenge = super::engine::new_nonce();
let expected =
super::evidence::derive_expected_response(&challenge, &identity.session_nonce);
let dir = std::env::temp_dir().join(format!(
"vetto-vng-ctl-{}-{}",
std::process::id(),
&super::engine::new_nonce()[..16]
));
std::fs::create_dir_all(&dir)?;
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700));
}
let downlink = dir.join(CHALLENGE_FIFO);
let uplink = dir.join(RESPONSE_FIFO);
let mkfifo = |path: &Path| -> std::io::Result<()> {
let cpath = std::ffi::CString::new({
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes().to_vec()
})
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
let rc = unsafe { libc::mkfifo(cpath.as_ptr(), 0o600) };
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
};
if let Err(e) = mkfifo(&downlink).and_then(|()| mkfifo(&uplink)) {
let _ = std::fs::remove_dir_all(&dir);
return Err(e);
}
let open_rw = |path: &Path, flags: i32| -> std::io::Result<std::os::fd::OwnedFd> {
let cpath = std::ffi::CString::new({
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes().to_vec()
})
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
let fd = unsafe { libc::open(cpath.as_ptr(), flags) };
if fd < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(unsafe {
use std::os::fd::FromRawFd;
std::os::fd::OwnedFd::from_raw_fd(fd)
})
};
let uplink_reader =
match open_rw(&uplink, libc::O_RDONLY | libc::O_NONBLOCK | libc::O_CLOEXEC) {
Ok(fd) => fd,
Err(e) => {
let _ = std::fs::remove_dir_all(&dir);
return Err(e);
}
};
let downlink_writer =
match open_rw(&downlink, libc::O_RDWR | libc::O_NONBLOCK | libc::O_CLOEXEC) {
Ok(fd) => fd,
Err(e) => {
let _ = std::fs::remove_dir_all(&dir);
return Err(e);
}
};
let line = format!("{challenge}\n");
let written = {
use std::os::fd::AsRawFd;
unsafe {
libc::write(
downlink_writer.as_raw_fd(),
line.as_ptr().cast(),
line.len(),
)
}
};
if written != line.len() as isize {
let _ = std::fs::remove_dir_all(&dir);
return Err(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"challenge buffer write incomplete",
));
}
Ok(Self {
dir,
downlink,
uplink,
expected,
uplink_reader,
_downlink_writer: downlink_writer,
})
}
pub fn env_entries(&self) -> [(String, String); 2] {
[
(
ENV_CONTROL_DOWNLINK.to_string(),
self.downlink.display().to_string(),
),
(
ENV_CONTROL_UPLINK.to_string(),
self.uplink.display().to_string(),
),
]
}
pub fn verify(
self,
identity: &ExecutionIdentity,
deadline: Instant,
) -> Option<super::evidence::VerifiedControl> {
use std::os::fd::AsRawFd;
let raw = self.uplink_reader.as_raw_fd();
let mut buf = Vec::new();
let mut tmp = [0u8; 128];
loop {
let n = unsafe { libc::read(raw, tmp.as_mut_ptr().cast(), tmp.len()) };
if n > 0 {
buf.extend_from_slice(&tmp[..n as usize]);
if buf.len() > MAX_CONTROL_BYTES {
return None;
}
if Instant::now() >= deadline {
break;
}
continue;
}
if n == 0 {
break; }
let err = std::io::Error::last_os_error();
match err.raw_os_error() {
Some(code) if code == libc::EAGAIN || code == libc::EWOULDBLOCK => {
if Instant::now() >= deadline {
break;
}
std::thread::sleep(Duration::from_millis(10));
continue;
}
Some(code) if code == libc::EINTR => continue,
_ => return None,
}
}
if buf.is_empty() {
return None;
}
while buf.last() == Some(&b'\n') || buf.last() == Some(&b'\r') {
buf.pop();
}
super::evidence::attest_control(identity, &self.expected, &buf)
}
}
#[cfg(unix)]
impl Drop for ControlChannel {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
#[cfg(not(unix))]
pub struct ControlChannel;
#[cfg(not(unix))]
impl ControlChannel {
pub fn create(_identity: &ExecutionIdentity) -> std::io::Result<Self> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"host-owned control channel requires unix FIFO",
))
}
pub fn env_entries(&self) -> [(String, String); 2] {
[
(ENV_CONTROL_DOWNLINK.to_string(), String::new()),
(ENV_CONTROL_UPLINK.to_string(), String::new()),
]
}
pub fn verify(
self,
_identity: &ExecutionIdentity,
_deadline: Instant,
) -> Option<super::evidence::VerifiedControl> {
None
}
}