Files
cubelinux-2/cubesys/src/bin/cubec.rs
T
CUBELinux-2 94bddda3dd feat(cubesys/cubecrypt): R4 challenge-response auth + R5/R6 wiring
- cubecrypt/src/auth.rs: HMAC-SHA256 signed HELLO (sign_hello/verify_hello),
  random_nonce_hex entropy source (plan R4)
- cube-server: --auth-key enables CHALLENGE/HELLO handshake; auth_handshake is
  a module-level free fn (run(self) consumes self, so the thread closure can
  only reach the captured psk). Resolves the earlier E0425 compile failure
- cubec.rs client: --auth-key builds a signed HELLO frame
- commands.rs: audit op constants + read-gating hooks wired into admit_read
- audit.rs: per-tenant append-only audit log in a Null-cube range (plan R6)
- clippy -D warnings clean; full ./check gate ALL CHECKS PASSED (61 tests)
2026-08-11 15:59:10 -04:00

120 lines
4.6 KiB
Rust

//! `cubec` — a tiny Unix-domain-socket client for the `cube-server` daemon.
//!
//! It speaks the same framed wire protocol as the server (`net` module): a
//! 4-byte length prefix + UTF-8 payload per frame, one request → one reply.
//!
//! When the daemon was started with `--auth-key`, the connection opens with a
//! `CHALLENGE <nonce>` frame (plan R4). `cubec` proves possession of the same
//! pre-shared key by replying `HELLO <tenant> <owner> <sig>`. Without a key
//! (legacy daemon) `cubec` behaves exactly as before — it sends its command
//! first and reads the reply.
use cubesys::net::{read_stream_frame, write_frame};
use std::io::{BufRead, Write};
use std::os::unix::net::UnixStream;
/// Resolve `--key VALUE` from argv, or None.
fn arg_value(args: &[String], key: &str) -> Option<String> {
args.iter()
.position(|a| a == key)
.and_then(|i| args.get(i + 1).cloned())
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let socket = arg_value(&args, "--socket")
.or_else(|| std::env::var("CUBE_SOCKET").ok())
.unwrap_or_else(|| "/run/cube/demo.sock".to_string());
let tenant = arg_value(&args, "--tenant").unwrap_or_else(|| "default".to_string());
let owner = arg_value(&args, "--owner")
.or_else(|| std::env::var("USER").ok())
.unwrap_or_else(|| "cubec".to_string());
// Pre-shared key for the signed-HELLO handshake (plan R4). `--auth-key PATH`
// reads a file; `--auth-key-env VAR` reads an env var; otherwise `CUBE_AUTH_KEY`.
let psk: Option<Vec<u8>> = if let Some(path) = arg_value(&args, "--auth-key") {
std::fs::read_to_string(&path)
.map(|s| s.trim().as_bytes().to_vec())
.ok()
} else if let Some(var) = arg_value(&args, "--auth-key-env") {
std::env::var(&var)
.ok()
.map(|s| s.trim().as_bytes().to_vec())
} else {
std::env::var("CUBE_AUTH_KEY")
.ok()
.map(|s| s.trim().as_bytes().to_vec())
};
let mut stream = match UnixStream::connect(&socket) {
Ok(s) => s,
Err(e) => {
eprintln!("cubec: cannot connect to {socket}: {e}");
std::process::exit(1);
}
};
// Complete the challenge-response handshake if we (and the server) are in
// auth mode. We only expect a CHALLENGE when we have a key; reading first
// unconditionally would deadlock against a legacy (un-keyed) server.
if let Some(key) = psk.as_deref() {
let frame = match read_stream_frame(&mut stream) {
Ok(f) => f,
Err(e) => {
eprintln!("cubec: handshake read failed: {e}");
std::process::exit(1);
}
};
let line = frame.trim();
if !line.to_lowercase().starts_with("challenge ") {
eprintln!("cubec: expected CHALLENGE from an auth-enabled server, got: {line}");
std::process::exit(1);
}
let nonce = line["challenge ".len()..].trim();
// HELLO + HMAC over (nonce|tenant|owner|remote). No remote asserted here.
let sig = cubecrypt::sign_hello(key, nonce, &tenant, &owner, None);
let hello = format!("HELLO {tenant} {owner} {sig}");
if write_frame(&mut stream, &hello).is_err() {
eprintln!("cubec: handshake write failed");
std::process::exit(1);
}
}
if args.is_empty() {
// REPL mode (legacy behaviour, now after an optional handshake).
let stdin = std::io::stdin();
for line in stdin.lock().lines().map_while(Result::ok) {
let line = line.trim();
if line.is_empty() {
continue;
}
if write_frame(&mut stream, line).is_err() {
eprintln!("cubec: write failed");
return;
}
match read_stream_frame(&mut stream) {
Ok(reply) => println!("{reply}"),
Err(e) => {
eprintln!("cubec: read failed: {e}");
return;
}
}
}
} else {
// One-shot: join the remaining args as the command line, send, print.
let cmd = args.join(" ");
if write_frame(&mut stream, &cmd).is_err() {
eprintln!("cubec: write failed");
std::process::exit(1);
}
match read_stream_frame(&mut stream) {
Ok(reply) => println!("{reply}"),
Err(e) => {
eprintln!("cubec: read failed: {e}");
std::process::exit(1);
}
}
}
let _ = stream.flush();
}