New crate: bascule-shell (471 lines, 1.8MB binary) Login shell that detects identity + platform attestation at startup. Wraps bash/zsh/fish — operator works normally, identity travels with them. Identity detection (priority order): 1. Entra via WSL2 interop 2. Azure CLI 3. Kerberos TGT 4. Cached OIDC token 5. System user (fallback) Platform attestation: TPM 2.0 PCR values via tpm2_pcrread (PCRs 0,1,2,7,10,14) IMA measurement log hash + count Keylime agent state Entra device compliance (WSL2 only) Composite SHA-256 hash over all evidence Shell features: Banner with identity + attestation summary BASCULE_* env vars injected into inner shell --info mode for dry-run display --json mode for machine-readable output --exec mode for single-command execution Configurable via ~/.config/bascule/shell.toml Tested on Fedora with real TPM 2.0: 6 PCRs successfully read from hardware All env vars propagated to inner shell 1.8MB binary, 0 substrate deps Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84 lines
2.1 KiB
Rust
84 lines
2.1 KiB
Rust
//! PTY bridge — spawns a shell and bridges I/O to the SSH channel.
|
|
|
|
use anyhow::{Context, Result};
|
|
use portable_pty::{native_pty_system, CommandBuilder, PtySize};
|
|
use std::io::{Read, Write};
|
|
|
|
/// Spawn a PTY with the given command and return the bridge.
|
|
pub fn spawn_pty(
|
|
command: &str,
|
|
args: &[String],
|
|
env: Vec<(String, String)>,
|
|
cols: u16,
|
|
rows: u16,
|
|
) -> Result<PtyBridge> {
|
|
let pty_system = native_pty_system();
|
|
|
|
let pair = pty_system
|
|
.openpty(PtySize {
|
|
rows,
|
|
cols,
|
|
pixel_width: 0,
|
|
pixel_height: 0,
|
|
})
|
|
.context("Failed to open PTY")?;
|
|
|
|
let mut cmd = CommandBuilder::new(command);
|
|
for arg in args {
|
|
cmd.arg(arg);
|
|
}
|
|
for (key, val) in &env {
|
|
cmd.env(key, val);
|
|
}
|
|
|
|
let child = pair
|
|
.slave
|
|
.spawn_command(cmd)
|
|
.context("Failed to spawn shell")?;
|
|
|
|
let reader = pair
|
|
.master
|
|
.try_clone_reader()
|
|
.context("Failed to clone PTY reader")?;
|
|
let writer = pair
|
|
.master
|
|
.take_writer()
|
|
.context("Failed to take PTY writer")?;
|
|
|
|
Ok(PtyBridge {
|
|
master: pair.master,
|
|
child,
|
|
reader: Some(reader),
|
|
writer,
|
|
})
|
|
}
|
|
|
|
/// A running PTY session with I/O handles.
|
|
pub struct PtyBridge {
|
|
pub master: Box<dyn portable_pty::MasterPty + Send>,
|
|
pub child: Box<dyn portable_pty::Child + Send + Sync>,
|
|
/// PTY reader — `take()` this to move it into a dedicated read thread.
|
|
pub reader: Option<Box<dyn Read + Send>>,
|
|
pub writer: Box<dyn Write + Send>,
|
|
}
|
|
|
|
impl PtyBridge {
|
|
/// Take the reader out of this bridge for use on a dedicated thread.
|
|
/// Returns None if already taken.
|
|
pub fn take_reader(&mut self) -> Option<Box<dyn Read + Send>> {
|
|
self.reader.take()
|
|
}
|
|
|
|
/// Resize the PTY.
|
|
pub fn resize(&self, cols: u16, rows: u16) -> Result<()> {
|
|
self.master
|
|
.resize(PtySize {
|
|
rows,
|
|
cols,
|
|
pixel_width: 0,
|
|
pixel_height: 0,
|
|
})
|
|
.context("Failed to resize PTY")?;
|
|
Ok(())
|
|
}
|
|
}
|