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:
CUBELinux-2
2026-08-11 15:59:10 -04:00
parent 78cd5c0046
commit 94bddda3dd
7 changed files with 710 additions and 159 deletions
+58
View File
@@ -11,6 +11,7 @@
//! survives restarts. Each `exec` takes and returns an `Arc<ConcurrentStore>`
//! so the server can hand a cloned handle to each worker thread.
use crate::audit::{Audit, OP_DELETE, OP_GRANT, OP_OPEN, OP_READ, OP_REVOKE, OP_SEAL, OP_WRITE};
use crate::grants::{grant, grant_allows, perms_from_str, revoke, Owner, Perm};
use crate::store::ConcurrentStore;
use crate::tenant::{TenantIdentity, TenantSession};
@@ -72,6 +73,11 @@ pub struct Session {
/// behaviour, so pre-existing `cubec`/stress.sh flows keep working until
/// the operator opts in via the daemon's `--require-identity` policy).
enforce_owner: bool,
/// Audit trail (plan R6). `Some` when the daemon enabled it on this
/// connection; every op is then appended (permitted or denied) so an
/// operator can later review who touched what. `None` keeps the library
/// REPL/tests silent (no audit record churn) unless explicitly enabled.
audit: Option<Audit>,
}
impl Default for Session {
@@ -90,6 +96,7 @@ impl Session {
identity: None,
txn: None,
enforce_owner: false,
audit: None,
}
}
@@ -102,6 +109,7 @@ impl Session {
identity: None,
txn: None,
enforce_owner: false,
audit: None,
}
}
@@ -118,6 +126,25 @@ impl Session {
identity: ts.identity(),
txn: None,
enforce_owner: false,
audit: None,
}
}
/// Enable the audit trail for this session (plan R6). Called by the daemon
/// once per connection after construction.
pub fn enable_audit(&mut self) {
self.audit = Some(Audit::new(self.store.clone()));
}
/// Append an audit entry if auditing is enabled. No-op otherwise.
fn audit_now(&self, op: u8, coord: Czyx, ok: bool) {
if let Some(a) = &self.audit {
let owner = self
.identity
.as_ref()
.map(|i| i.owner_local.as_str())
.unwrap_or("<anonymous>");
a.append(op, coord, owner, ok);
}
}
@@ -372,6 +399,23 @@ impl Session {
Ok("ok: transaction rolled back".to_string())
}
"stats" => Ok(self.stats()),
"audit" => {
// Plan R6: dump the append-only audit trail for this session's
// store. Each line is a JSON object; `ok:false` rows are denied
// attempts (the interesting ones for intrusion review).
let log = match &self.audit {
Some(a) => a.dump(),
None => return Err(
"audit: audit trail is not enabled on this session (daemon --enable-audit)"
.to_string(),
),
};
if log.trim().is_empty() {
Ok("audit -> (no entries)".to_string())
} else {
Ok(format!("audit ->\n{log}"))
}
}
"query" => {
let dt = it
.next()
@@ -412,8 +456,10 @@ impl Session {
let coord = scratch_code_coord(path, Kind::Fn, name, &ops)?;
// Task 6: reject overwriting a record owned by a different owner.
if let Some(msg) = self.admit_mutate(coord, Perm::Write) {
self.audit_now(OP_WRITE, coord, false);
return Err(msg);
}
self.audit_now(OP_WRITE, coord, true);
let owner = self.identity.as_ref().map(|i| i.owner_local.as_str());
let value = {
let mut scratch = CubeStore::new(HashBackend::new());
@@ -452,8 +498,10 @@ impl Session {
let name = path.rsplit('/').next().unwrap_or(path);
let coord = scratch_code_coord(path, Kind::Fn, name, &code)?;
if let Some(msg) = self.admit_mutate(coord, Perm::Write) {
self.audit_now(OP_WRITE, coord, false);
return Err(msg);
}
self.audit_now(OP_WRITE, coord, true);
let owner = self.identity.as_ref().map(|i| i.owner_local.as_str());
let value = {
let mut scratch = CubeStore::new(HashBackend::new());
@@ -483,8 +531,10 @@ impl Session {
// Task 6: reject deleting a record owned by a different owner
// (unless unowned, which any identity may take over).
if let Some(msg) = self.admit_mutate(coord, Perm::Write) {
self.audit_now(OP_DELETE, coord, false);
return Err(msg);
}
self.audit_now(OP_DELETE, coord, true);
if let Some(txn) = self.txn.as_mut() {
txn.ops.push(TxnOp { coord, put: None });
return Ok(format!("buffered del {path} — commit to apply"));
@@ -537,6 +587,7 @@ impl Session {
};
let seq = grant(&self.store, &granter, &grantee, perms, scope)
.map_err(|e| format!("grant: {e}"))?;
self.audit_now(OP_GRANT, scope.unwrap_or(Czyx::new(0, 0, 0, 0)), true);
let scope_s = match scope {
Some(c) => c.pack_u32().to_string(),
None => "global".to_string(),
@@ -582,6 +633,7 @@ impl Session {
remote: gr,
};
let removed = revoke(&self.store, &granter, &grantee, scope);
self.audit_now(OP_REVOKE, scope.unwrap_or(Czyx::new(0, 0, 0, 0)), true);
Ok(format!("ok: revoked {removed} grant(s)"))
}
"run" => {
@@ -590,8 +642,10 @@ impl Session {
// R5: reads (this one also *executes*) honor the owner/grantee
// read gate, same as writes honor the mutate gate.
if let Some(msg) = self.admit_read(_coord) {
self.audit_now(OP_READ, _coord, false);
return Err(msg);
}
self.audit_now(OP_READ, _coord, true);
let sn = txn_snapshot(self);
let cell = crate::load_code_cell(&sn, path).map_err(|e| e.to_string())?;
let mut vm = Vm::new(sn);
@@ -622,8 +676,10 @@ impl Session {
// R5: metadata reads honor the read gate.
let coord = crate::path_to_czyx(path).map_err(|e| e.to_string())?;
if let Some(msg) = self.admit_read(coord) {
self.audit_now(OP_READ, coord, false);
return Err(msg);
}
self.audit_now(OP_READ, coord, true);
let sn = txn_snapshot(self);
let fs = cubefs::CubeFs::new(sn);
let a = fs
@@ -659,6 +715,8 @@ impl Session {
if let Some(msg) = self.admit_mutate(coord, Perm::Write) {
return Err(msg);
}
// Audit the destructive op that is about to run (seal vs open).
self.audit_now(if cmd == "seal" { OP_SEAL } else { OP_OPEN }, coord, true);
if store.get_record(&kc).is_none() {
store.put_raw(kc, b"demo-key-material-32-bytes-long!!".to_vec());