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)
This commit is contained in:
+98
-70
@@ -1,91 +1,119 @@
|
||||
//! `cubec` — the CUBELinux-2 system client.
|
||||
//! `cubec` — a tiny Unix-domain-socket client for the `cube-server` daemon.
|
||||
//!
|
||||
//! Talks to `cube-server` over its Unix-domain socket. Two modes:
|
||||
//! 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.
|
||||
//!
|
||||
//! cubec <command...> one-shot: send a single command, print the reply
|
||||
//! cubec REPL: read command lines from stdin, one per
|
||||
//! connection, printing each reply (like `cube repl`)
|
||||
//!
|
||||
//! The socket defaults to /run/cube/cube.sock (or $XDG_RUNTIME_DIR/cube/cube.sock)
|
||||
//! and can be overridden with `--socket PATH`.
|
||||
//! 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;
|
||||
|
||||
use cubesys::net::{read_frame, write_frame};
|
||||
/// 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().collect();
|
||||
let socket_path = socket_from_args(&args);
|
||||
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())
|
||||
};
|
||||
|
||||
// Build the command from everything that isn't `--socket PATH`.
|
||||
let mut rest: Vec<String> = Vec::new();
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
if args[i] == "--socket" {
|
||||
i += 2; // skip the flag and its value
|
||||
continue;
|
||||
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);
|
||||
}
|
||||
rest.push(args[i].clone());
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if rest.is_empty() {
|
||||
repl(&socket_path);
|
||||
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 {
|
||||
let cmd = rest.join(" ");
|
||||
match request(&socket_path, &cmd) {
|
||||
// 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: {e}");
|
||||
eprintln!("cubec: read failed: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn repl(socket_path: &str) {
|
||||
let stdin = std::io::stdin();
|
||||
for line in stdin.lock().lines() {
|
||||
let line = match line {
|
||||
Ok(l) => l,
|
||||
Err(_) => break,
|
||||
};
|
||||
if line.trim().is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
match request(socket_path, &line) {
|
||||
Ok(reply) => println!("{reply}"),
|
||||
Err(e) => eprintln!("error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a connection, send one command frame, return the reply frame.
|
||||
fn request(socket_path: &str, command: &str) -> Result<String, String> {
|
||||
let mut stream = UnixStream::connect(socket_path)
|
||||
.map_err(|e| format!("cannot connect to {socket_path}: {e}"))?;
|
||||
write_frame(&mut stream, command).map_err(|e| format!("write: {e}"))?;
|
||||
read_frame(&mut stream).map_err(|e| format!("read: {e}"))
|
||||
}
|
||||
|
||||
fn socket_from_args(args: &[String]) -> String {
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
if args[i] == "--socket" {
|
||||
if let Some(v) = args.get(i + 1) {
|
||||
return v.clone();
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
|
||||
return format!("{runtime}/cube/cube.sock");
|
||||
}
|
||||
"/run/cube/cube.sock".to_string()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn _flush() {
|
||||
let _ = std::io::stdout().flush();
|
||||
let _ = stream.flush();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user