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,194 @@
|
||||
//! Challenge-response authentication for the daemon HELLO frame (plan R4).
|
||||
//!
|
||||
//! The daemon's `HELLO` identity is, by itself, self-asserted — any client can
|
||||
//! claim to be `alice` on `tenant-x`. On a single-owner box behind a private
|
||||
//! LAN that is acceptable, but the moment the daemon is reachable by anything
|
||||
//! other than a trusted local process, an impersonator can forge an identity.
|
||||
//!
|
||||
//! R4 closes that with a pre-shared-key (PSK) mutual-auth handshake:
|
||||
//!
|
||||
//! 1. On connect, if the server has a PSK configured (`--auth-key`), it sends
|
||||
//! `CHALLENGE <hexnonce>` (32 random bytes).
|
||||
//! 2. The client proves possession of the PSK by replying
|
||||
//! `HELLO <tenant> <owner_local> [<owner_remote>] <hexsig>` where
|
||||
//! `hexsig = HMAC-SHA256(psk, nonce || "|" || tenant || "|" ||
|
||||
//! owner_local || "|" || owner_remote)`.
|
||||
//! 3. The server recomputes the HMAC and rejects the connection on mismatch.
|
||||
//!
|
||||
//! The nonce makes each signature single-use, so a sniffed handshake cannot be
|
||||
//! replayed. The HMAC key is the PSK (never sent over the wire). We implement
|
||||
//! HMAC-SHA256 with the `sha2` crate already in this crate's dependency tree —
|
||||
//! no new dependency, and we never roll a cipher.
|
||||
//!
|
||||
//! Without a configured PSK the server skips the challenge entirely and
|
||||
//! behaves exactly as before (legacy `cubec`/`stress.sh` keep working).
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Block size of SHA-256, used by the HMAC inner/outer padding construction.
|
||||
const SHA256_BLOCK: usize = 64;
|
||||
|
||||
/// HMAC-SHA256 over `msg` with key `key`. Implemented directly from RFC 2104
|
||||
/// using two SHA-256 passes — no external `hmac` crate needed.
|
||||
pub fn hmac_sha256(key: &[u8], msg: &[u8]) -> [u8; 32] {
|
||||
// Keys longer than the block are hashed first (RFC 2104 step 1).
|
||||
let mut k = [0u8; SHA256_BLOCK];
|
||||
if key.len() > SHA256_BLOCK {
|
||||
let h = Sha256::digest(key);
|
||||
k[..32].copy_from_slice(&h);
|
||||
} else {
|
||||
k[..key.len()].copy_from_slice(key);
|
||||
}
|
||||
|
||||
let ipad: [u8; SHA256_BLOCK] = core::array::from_fn(|i| k[i] ^ 0x36);
|
||||
let opad: [u8; SHA256_BLOCK] = core::array::from_fn(|i| k[i] ^ 0x5c);
|
||||
|
||||
let mut inner = Sha256::new();
|
||||
inner.update(ipad);
|
||||
inner.update(msg);
|
||||
let inner_h = inner.finalize();
|
||||
|
||||
let mut outer = Sha256::new();
|
||||
outer.update(opad);
|
||||
outer.update(inner_h);
|
||||
let outer_h = outer.finalize();
|
||||
|
||||
let mut out = [0u8; 32];
|
||||
out.copy_from_slice(&outer_h);
|
||||
out
|
||||
}
|
||||
|
||||
/// The exact message string signed for a HELLO frame, given the server's nonce
|
||||
/// and the asserted identity fields. `owner_remote` is `""` when absent so the
|
||||
/// canonical string is unambiguous (no optional-trailing-separator ambiguity).
|
||||
fn hello_msg(nonce_hex: &str, tenant: &str, owner_local: &str, owner_remote: &str) -> String {
|
||||
format!("{}|{}|{}|{}", nonce_hex, tenant, owner_local, owner_remote)
|
||||
}
|
||||
|
||||
/// Produce the hex signature a client should send after receiving a challenge.
|
||||
pub fn sign_hello(
|
||||
psk: &[u8],
|
||||
nonce_hex: &str,
|
||||
tenant: &str,
|
||||
owner_local: &str,
|
||||
owner_remote: Option<&str>,
|
||||
) -> String {
|
||||
let remote = owner_remote.unwrap_or("");
|
||||
let msg = hello_msg(nonce_hex, tenant, owner_local, remote);
|
||||
let sig = hmac_sha256(psk, msg.as_bytes());
|
||||
hex_encode(&sig)
|
||||
}
|
||||
|
||||
/// Verify a client-sent signature against the expected PSK + nonce + identity.
|
||||
/// Constant-ish compare (not a timing-safe compare — the threat here is
|
||||
/// impersonation, not a remote timing oracle over a private LAN; for a
|
||||
/// stronger guarantee swap in a `subtle::ConstantTimeEq`). Returns true on
|
||||
/// match.
|
||||
pub fn verify_hello(
|
||||
psk: &[u8],
|
||||
nonce_hex: &str,
|
||||
tenant: &str,
|
||||
owner_local: &str,
|
||||
owner_remote: Option<&str>,
|
||||
sig_hex: &str,
|
||||
) -> bool {
|
||||
let expected = sign_hello(psk, nonce_hex, tenant, owner_local, owner_remote);
|
||||
expected.eq_ignore_ascii_case(sig_hex)
|
||||
}
|
||||
|
||||
/// Lowercase hex encode (no allocation beyond the result).
|
||||
pub fn hex_encode(bytes: &[u8]) -> String {
|
||||
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||||
let mut s = String::with_capacity(bytes.len() * 2);
|
||||
for &b in bytes {
|
||||
s.push(HEX[(b >> 4) as usize] as char);
|
||||
s.push(HEX[(b & 0x0f) as usize] as char);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Generate a random nonce as a hex string, reading `n` bytes from the OS
|
||||
/// CSPRNG (`/dev/urandom` on Linux/Unix). Dependency-free; used by the daemon
|
||||
/// to mint per-connection `CHALLENGE` nonces for the HELLO handshake. Returns
|
||||
/// an error only if the entropy source cannot be opened/read.
|
||||
pub fn random_nonce_hex(n: usize) -> std::io::Result<String> {
|
||||
use std::io::Read;
|
||||
let mut buf = vec![0u8; n];
|
||||
let mut f = std::fs::File::open("/dev/urandom")?;
|
||||
f.read_exact(&mut buf)?;
|
||||
Ok(hex_encode(&buf))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hmac_sha256_known_vector() {
|
||||
// RFC 4231 Test Case 1: key="Jefe", data="what do ya want for nothing?".
|
||||
let key = b"Jefe";
|
||||
let data = b"what do ya want for nothing?";
|
||||
let out = hmac_sha256(key, data);
|
||||
// Expected (RFC 4231 Test Case 1, HMAC-SHA256):
|
||||
// 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
|
||||
let want = "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843";
|
||||
assert_eq!(hex_encode(&out), want, "HMAC-SHA256 RFC4231 TC1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_sha256_long_key_is_hashed_first() {
|
||||
// Key longer than the 64-byte block must be pre-hashed (RFC 2104).
|
||||
let key = vec![0xaa; 80];
|
||||
let data = b"test message";
|
||||
let out = hmac_sha256(&key, data);
|
||||
// Same input via a 32-byte pre-hash path should be stable.
|
||||
let key32 = Sha256::digest(&key);
|
||||
let out2 = hmac_sha256(&key32, data);
|
||||
assert_eq!(out, out2, "long key pre-hash path matches");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hello_sign_verify_roundtrips() {
|
||||
let psk = b"shared-secret-for-lan-daemon";
|
||||
let nonce = "deadbeefcafe1234";
|
||||
let sig = sign_hello(psk, nonce, "agent-a", "alice", None);
|
||||
assert!(verify_hello(psk, nonce, "agent-a", "alice", None, &sig));
|
||||
// Wrong owner must fail.
|
||||
assert!(!verify_hello(psk, nonce, "agent-a", "mallory", None, &sig));
|
||||
// Wrong nonce must fail.
|
||||
assert!(!verify_hello(
|
||||
psk,
|
||||
"differentnonce",
|
||||
"agent-a",
|
||||
"alice",
|
||||
None,
|
||||
&sig
|
||||
));
|
||||
// With remote present.
|
||||
let sig_r = sign_hello(psk, nonce, "agent-a", "alice", Some("remote-x"));
|
||||
assert!(verify_hello(
|
||||
psk,
|
||||
nonce,
|
||||
"agent-a",
|
||||
"alice",
|
||||
Some("remote-x"),
|
||||
&sig_r
|
||||
));
|
||||
assert!(!verify_hello(
|
||||
psk,
|
||||
nonce,
|
||||
"agent-a",
|
||||
"alice",
|
||||
Some("other-remote"),
|
||||
&sig_r
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_is_nonce_dependent() {
|
||||
let psk = b"k";
|
||||
let a = sign_hello(psk, "nonce1", "t", "o", None);
|
||||
let b = sign_hello(psk, "nonce2", "t", "o", None);
|
||||
assert_ne!(a, b, "different nonces must yield different signatures");
|
||||
}
|
||||
}
|
||||
@@ -31,9 +31,11 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod auth;
|
||||
pub mod env;
|
||||
pub mod transform;
|
||||
|
||||
pub use auth::{hex_encode, hmac_sha256, random_nonce_hex, sign_hello, verify_hello};
|
||||
pub use env::{CubeEnv, EnvError, Selector, HEADER_FLAG_ENCRYPTED};
|
||||
pub use transform::{CryptoError, Key, KeySlot, TransformId};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user