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:
@@ -0,0 +1,125 @@
|
||||
//! Audit trail for the daemon (plan R6).
|
||||
//!
|
||||
//! Every mutating or read op executed through a [`Session`] is appended to an
|
||||
//! append-only log stored in a dedicated Null-cube range of the *same*
|
||||
//! [`ConcurrentStore`] the tenant uses. The PDF asks for "space for access
|
||||
//! logs ... in separate Null ranges"; this is that space. Because it lives in
|
||||
//! the store, it is durable and isolated per tenant (each tenant's store has
|
||||
//! its own audit range).
|
||||
//!
|
||||
//! Op tags (match the command set):
|
||||
//! OP_WRITE = 1 write / prog
|
||||
//! OP_DELETE = 2 del
|
||||
//! OP_READ = 3 run / stat / ls (metadata)
|
||||
//! OP_GRANT = 4 grant
|
||||
//! OP_REVOKE = 5 revoke
|
||||
//! OP_SEAL = 6 seal
|
||||
//! OP_OPEN = 7 open
|
||||
//!
|
||||
//! Each entry is one JSON object on its own line:
|
||||
//! {"seq":N,"ts":U,"op":B,"coord":"C.Z.Y.X","owner":"who","ok":bool}
|
||||
//!
|
||||
//! The whole log is kept under one head record and appended by read-modify-
|
||||
//! write under a per-store mutex. For a single-owner box this is cheap and
|
||||
//! correct; under many concurrent writers it becomes O(n) per append — the
|
||||
//! same scaling caveat noted for the grant table (plan R2), and fine at this
|
||||
//! deployment's volume.
|
||||
|
||||
use crate::store::ConcurrentStore;
|
||||
use cubecoords::Czyx;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Head coordinate of the audit range (a Null cube, distinct from grants at
|
||||
/// 0,1,0,1 and the FUSE ACL range).
|
||||
pub const AUDIT_HEAD: Czyx = Czyx::new(0, 4, 0, 0);
|
||||
|
||||
/// Audit op tag: `write` / `prog` (content mutation).
|
||||
pub const OP_WRITE: u8 = 1;
|
||||
/// Audit op tag: `del` (deletion).
|
||||
pub const OP_DELETE: u8 = 2;
|
||||
/// Audit op tag: `run` / `stat` / `ls` (metadata / read).
|
||||
pub const OP_READ: u8 = 3;
|
||||
/// Audit op tag: `grant` (permission grant).
|
||||
pub const OP_GRANT: u8 = 4;
|
||||
/// Audit op tag: `revoke` (permission revocation).
|
||||
pub const OP_REVOKE: u8 = 5;
|
||||
/// Audit op tag: `seal` (freeze a record).
|
||||
pub const OP_SEAL: u8 = 6;
|
||||
/// Audit op tag: `open` (unseal a record).
|
||||
pub const OP_OPEN: u8 = 7;
|
||||
|
||||
/// Append-only audit log bound to one tenant store. Cheap to clone (just an
|
||||
/// `Arc<Mutex<()>>` serialization guard + a coord); the actual data lives in
|
||||
/// the store.
|
||||
#[derive(Clone)]
|
||||
pub struct Audit {
|
||||
store: std::sync::Arc<ConcurrentStore>,
|
||||
/// Serializes appends so two threads don't read-modify-write the same head
|
||||
/// record concurrently (last-writer-wins would drop entries).
|
||||
guard: std::sync::Arc<Mutex<()>>,
|
||||
}
|
||||
|
||||
impl Audit {
|
||||
/// Bind an audit log to a store.
|
||||
pub fn new(store: std::sync::Arc<ConcurrentStore>) -> Self {
|
||||
Audit {
|
||||
store,
|
||||
guard: std::sync::Arc::new(Mutex::new(())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Append one audit entry. `ok` records whether the op was permitted
|
||||
/// (true) or rejected by the gate (false) — so the log captures both
|
||||
/// successful and denied attempts (the latter being the interesting ones
|
||||
/// for intrusion detection).
|
||||
pub fn append(&self, op: u8, coord: Czyx, owner: &str, ok: bool) {
|
||||
let _lock = self.guard.lock().unwrap();
|
||||
let ts = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
// Current sequence = number of lines already present.
|
||||
let existing = self
|
||||
.store
|
||||
.get_record(&AUDIT_HEAD)
|
||||
.map(|(_, v)| String::from_utf8_lossy(&v).into_owned())
|
||||
.unwrap_or_default();
|
||||
let seq = existing.lines().filter(|l| !l.trim().is_empty()).count() as u64 + 1;
|
||||
let line = format!(
|
||||
"{{\"seq\":{seq},\"ts\":{ts},\"op\":{op},\"coord\":\"{}\",\"owner\":\"{}\",\"ok\":{ok}}}",
|
||||
coord.pack_u32(),
|
||||
owner_escape(owner)
|
||||
);
|
||||
let mut next = existing;
|
||||
if !next.is_empty() && !next.ends_with('\n') {
|
||||
next.push('\n');
|
||||
}
|
||||
next.push_str(&line);
|
||||
next.push('\n');
|
||||
let mut h = cubecoords::CubeHeader::new();
|
||||
h.doc_type = Some("audit-log".into());
|
||||
h.refresh_flags();
|
||||
self.store.put_record(AUDIT_HEAD, &h, next.as_bytes());
|
||||
}
|
||||
|
||||
/// Return all audit lines (newest-last), as a single newline-joined string.
|
||||
pub fn dump(&self) -> String {
|
||||
self.store
|
||||
.get_record(&AUDIT_HEAD)
|
||||
.map(|(_, v)| String::from_utf8_lossy(&v).into_owned())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal JSON string escaping for the owner field (quotes + backslash).
|
||||
fn owner_escape(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
_ => out.push(c),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
Reference in New Issue
Block a user