- 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)
120 lines
4.6 KiB
Rust
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();
|
|
}
|