Files
wifi-densepose/crates/cognitum-gate-kernel/docs/SECURITY_AUDIT.md
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

683 lines
20 KiB
Markdown

# Security Audit Report: Cognitum Gate Implementation
**Audit Date:** 2026-01-17
**Auditor:** Claude Code Security Review Agent
**Scope:** cognitum-gate-kernel, cognitum-gate-tilezero, mcp-gate
**Risk Classification:** Uses CVSS-style severity (Critical/High/Medium/Low)
---
## Executive Summary
This security audit identified **17 security issues** across the cognitum-gate implementation:
| Severity | Count | Categories |
|----------|-------|------------|
| Critical | 2 | Cryptographic bypass, signature truncation |
| High | 4 | Memory safety, unsafe code, race conditions |
| Medium | 6 | Input validation, integer overflow, DoS vectors |
| Low | 5 | Information disclosure, edge cases |
**Recommendation:** The Critical issues in `permit.rs` must be fixed before production deployment as they completely bypass signature verification.
---
## Critical Issues
### CGK-001: Signature Verification Bypass (CRITICAL)
**Severity:** Critical
**Location:** `/home/user/ruvector/crates/cognitum-gate-tilezero/src/permit.rs:136-153`
**CVSS:** 9.8 (Critical)
**Description:**
The `Verifier::verify()` function does not actually verify signatures. It computes a hash from the token content and compares it to... the same hash computed from the same content. This comparison always succeeds.
```rust
// Lines 147-151 - BROKEN VERIFICATION
let expected_hash = blake3::hash(&content);
if hash.as_bytes() != expected_hash.as_bytes() {
return Err(VerifyError::HashMismatch);
}
// hash == expected_hash ALWAYS - computed from same content!
```
**Impact:**
Any attacker can forge permit tokens. The cryptographic authentication is completely bypassed. All gate decisions can be spoofed.
**Recommended Fix:**
```rust
pub fn verify(&self, token: &PermitToken) -> Result<(), VerifyError> {
let content = token.signable_content();
let hash = blake3::hash(&content);
// Reconstruct full 64-byte signature
// REQUIRES: Store full signature in token, not truncated 32 bytes
let signature = ed25519_dalek::Signature::from_bytes(&token.signature)
.map_err(|_| VerifyError::SignatureFailed)?;
// Actually verify the signature
self.verifying_key
.verify(hash.as_bytes(), &signature)
.map_err(|_| VerifyError::SignatureFailed)?;
Ok(())
}
```
---
### CGK-002: Ed25519 Signature Truncation (CRITICAL)
**Severity:** Critical
**Location:** `/home/user/ruvector/crates/cognitum-gate-tilezero/src/permit.rs:103-111`
**CVSS:** 9.1 (Critical)
**Description:**
The `sign_token` function truncates the 64-byte Ed25519 signature to 32 bytes:
```rust
// Line 109 - Discards half the signature!
token.mac.copy_from_slice(&signature.to_bytes()[..32]);
```
Ed25519 signatures are 64 bytes. Truncating to 32 bytes makes reconstruction impossible and verification meaningless.
**Impact:**
Combined with CGK-001, this makes signature verification completely non-functional. Even if verification was fixed, the stored signature cannot be reconstructed.
**Recommended Fix:**
```rust
// In PermitToken struct - change mac field:
pub signature: [u8; 64], // Full Ed25519 signature
// In sign_token:
token.signature.copy_from_slice(&signature.to_bytes());
```
---
## High Severity Issues
### CGK-003: Unsafe Global Mutable State Without Synchronization
**Severity:** High
**Location:** `/home/user/ruvector/crates/cognitum-gate-kernel/src/lib.rs:413`
**CVSS:** 7.5
**Description:**
The global `TILE_STATE` is accessed through `static mut` without any synchronization primitives:
```rust
static mut TILE_STATE: Option<TileState> = None;
```
All WASM export functions (`init_tile`, `ingest_delta`, `tick`, etc.) access this mutable static unsafely.
**Impact:**
In multi-threaded contexts or if WASM threading is enabled, this creates data races leading to undefined behavior, memory corruption, or security bypasses.
**Recommended Fix:**
```rust
use core::cell::UnsafeCell;
use core::sync::atomic::{AtomicBool, Ordering};
struct TileStateHolder {
initialized: AtomicBool,
state: UnsafeCell<Option<TileState>>,
}
// Or for single-threaded WASM, use OnceCell pattern
static TILE_STATE: once_cell::sync::OnceCell<RefCell<TileState>> = OnceCell::new();
```
---
### CGK-004: Unsafe Raw Pointer Dereference Without Validation
**Severity:** High
**Location:** `/home/user/ruvector/crates/cognitum-gate-kernel/src/lib.rs:207-210`
**CVSS:** 7.3
**Description:**
The `ingest_delta_raw` function casts a raw pointer without checking alignment:
```rust
pub unsafe fn ingest_delta_raw(&mut self, ptr: *const u8) -> bool {
let delta = unsafe { &*(ptr as *const Delta) }; // No alignment check!
self.ingest_delta(delta)
}
```
`Delta` likely requires alignment > 1 byte. Misaligned access is undefined behavior.
**Impact:**
Misaligned memory access causes undefined behavior on some architectures, potentially leading to crashes or exploitable memory corruption.
**Recommended Fix:**
```rust
pub unsafe fn ingest_delta_raw(&mut self, ptr: *const u8) -> bool {
// Check alignment
if (ptr as usize) % core::mem::align_of::<Delta>() != 0 {
return false;
}
// Check null
if ptr.is_null() {
return false;
}
let delta = unsafe { &*(ptr as *const Delta) };
self.ingest_delta(delta)
}
```
---
### CGK-005: Bump Allocator Race Condition
**Severity:** High
**Location:** `/home/user/ruvector/crates/cognitum-gate-kernel/src/lib.rs:70-99`
**CVSS:** 7.0
**Description:**
The bump allocator uses static mutable variables without synchronization:
```rust
static mut HEAP: [u8; HEAP_SIZE] = [0; HEAP_SIZE];
static mut HEAP_PTR: usize = 0;
unsafe impl GlobalAlloc for BumpAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe {
let aligned = (HEAP_PTR + align - 1) & !(align - 1); // Race condition!
// ...
HEAP_PTR = aligned + size; // Non-atomic update!
}
}
}
```
**Impact:**
Concurrent allocations could return overlapping memory regions, leading to memory corruption.
**Recommended Fix:**
```rust
use core::sync::atomic::{AtomicUsize, Ordering};
static HEAP_PTR: AtomicUsize = AtomicUsize::new(0);
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
loop {
let current = HEAP_PTR.load(Ordering::Acquire);
let aligned = (current + layout.align() - 1) & !(layout.align() - 1);
let new_ptr = aligned + layout.size();
if new_ptr > HEAP_SIZE {
return core::ptr::null_mut();
}
if HEAP_PTR.compare_exchange_weak(current, new_ptr, Ordering::Release, Ordering::Relaxed).is_ok() {
return unsafe { HEAP.as_mut_ptr().add(aligned) };
}
}
}
```
---
### CGK-006: Unchecked Union Access in Delta Processing
**Severity:** High
**Location:** `/home/user/ruvector/crates/cognitum-gate-kernel/src/lib.rs:288-321`
**CVSS:** 6.8
**Description:**
The `apply_delta` function uses unsafe union access based on a tag field:
```rust
DeltaTag::EdgeAdd => {
let ea = unsafe { delta.get_edge_add() }; // Trusts tag
// ...
}
```
If the tag is corrupted or maliciously set, accessing the wrong union variant leads to undefined behavior.
**Impact:**
A malformed delta with mismatched tag/data could cause memory corruption or information disclosure.
**Recommended Fix:**
- Add validation of delta integrity (checksum/hash)
- Use a safe enum representation instead of tagged union where possible
- Add bounds checking on union field values after extraction
---
## Medium Severity Issues
### CGK-007: Division by Zero in Threshold Computation
**Severity:** Medium
**Location:** `/home/user/ruvector/crates/cognitum-gate-tilezero/src/decision.rs:223-228`
**CVSS:** 5.9
**Description:**
Pre-computed reciprocals can cause division by zero:
```rust
let inv_min_cut = 1.0 / thresholds.min_cut; // Zero if min_cut == 0
let inv_max_shift = 1.0 / thresholds.max_shift; // Zero if max_shift == 0
let inv_tau_range = 1.0 / (thresholds.tau_permit - thresholds.tau_deny); // Zero if equal
```
**Impact:**
Results in infinity/NaN values that propagate through decision logic, potentially causing incorrect permit/deny decisions.
**Recommended Fix:**
```rust
pub fn new(thresholds: GateThresholds) -> Result<Self, ThresholdError> {
if thresholds.min_cut == 0.0 || thresholds.max_shift == 0.0 {
return Err(ThresholdError::ZeroThreshold);
}
if (thresholds.tau_permit - thresholds.tau_deny).abs() < f64::EPSILON {
return Err(ThresholdError::EqualTauRange);
}
// ... continue with safe reciprocal computation
}
```
---
### CGK-008: Integer Overflow in Token TTL Check
**Severity:** Medium
**Location:** `/home/user/ruvector/crates/cognitum-gate-tilezero/src/permit.rs:31-33`
**CVSS:** 5.3
**Description:**
The validity check can overflow:
```rust
pub fn is_valid_time(&self, now_ns: u64) -> bool {
now_ns <= self.timestamp + self.ttl_ns // Overflow possible!
}
```
If `timestamp + ttl_ns` overflows u64, the comparison becomes incorrect.
**Impact:**
Tokens with very large timestamps or TTLs could have incorrect validity checks, either expiring immediately or never expiring.
**Recommended Fix:**
```rust
pub fn is_valid_time(&self, now_ns: u64) -> bool {
self.timestamp.checked_add(self.ttl_ns)
.map(|expiry| now_ns <= expiry)
.unwrap_or(true) // If overflow, consider perpetually valid or use saturating
}
```
---
### CGK-009: Unbounded History Growth / DoS Vector
**Severity:** Medium
**Location:** `/home/user/ruvector/crates/cognitum-gate-tilezero/src/receipt.rs:124-132, 169-185`
**CVSS:** 5.0
**Description:**
The `ReceiptLog` uses a HashMap that grows unboundedly:
```rust
pub struct ReceiptLog {
receipts: HashMap<u64, WitnessReceipt>, // Grows forever
// ...
}
```
Additionally, `verify_chain_to` iterates from 0 to sequence number, making it O(n) in chain length.
**Impact:**
Memory exhaustion attack by generating many decisions. Chain verification becomes increasingly slow.
**Recommended Fix:**
```rust
const MAX_RECEIPTS: usize = 100_000;
pub fn append(&mut self, receipt: WitnessReceipt) -> Result<(), LogFullError> {
if self.receipts.len() >= MAX_RECEIPTS {
// Implement pruning or return error
self.prune_old_receipts();
}
// ...
}
// Use rolling window verification instead of full chain
pub fn verify_recent(&self, window: usize) -> Result<(), ChainVerifyError> {
let start = self.latest_sequence.saturating_sub(window as u64);
// Verify only recent entries
}
```
---
### CGK-010: Unchecked Array Index in Evidence Processing
**Severity:** Medium
**Location:** `/home/user/ruvector/crates/cognitum-gate-kernel/src/evidence.rs:407-416`
**CVSS:** 4.8
**Description:**
Window access uses unchecked indexing:
```rust
let idx = self.window_head as usize;
// Line 410: Assumes idx < WINDOW_SIZE
unsafe {
*self.window.get_unchecked_mut(idx) = ObsRecord { obs, tick };
}
```
The bit masking on line 413 is correct, but it happens AFTER the unsafe access.
**Impact:**
If `window_head` is corrupted, out-of-bounds write occurs.
**Recommended Fix:**
```rust
// Apply mask BEFORE access
let idx = (self.window_head as usize) & (WINDOW_SIZE - 1);
self.window[idx] = ObsRecord { obs, tick }; // Safe bounds-checked access
self.window_head = (self.window_head + 1) as u16;
```
---
### CGK-011: Panic on System Time Before Epoch
**Severity:** Medium
**Location:** `/home/user/ruvector/crates/cognitum-gate-tilezero/src/lib.rs:173-176`
**CVSS:** 4.5
**Description:**
The time computation can panic:
```rust
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap() // Panics if system time < epoch!
.as_nanos() as u64;
```
**Impact:**
If system time is misconfigured (before 1970), the gate panics and becomes unavailable.
**Recommended Fix:**
```rust
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(std::time::Duration::ZERO)
.as_nanos() as u64;
```
---
### CGK-012: Processing Rate Division by Zero
**Severity:** Medium
**Location:** `/home/user/ruvector/crates/cognitum-gate-kernel/src/report.rs:284-289`
**CVSS:** 4.0
**Description:**
```rust
pub fn processing_rate(&self) -> f32 {
if self.tick_time_us == 0 {
0.0 // Handled correctly
} else {
(self.deltas_processed as f32) / (self.tick_time_us as f32)
}
}
```
This is actually handled correctly. However, the check should use floating point division behavior documentation.
**Status:** No action required - correctly implemented.
---
## Low Severity Issues
### CGK-013: Tick Time Truncation
**Severity:** Low
**Location:** `/home/user/ruvector/crates/cognitum-gate-kernel/src/lib.rs:257`
**CVSS:** 3.5
**Description:**
Tick time is cast from u32 to u16:
```rust
report.tick_time_us = (tick_end - tick_start) as u16; // Truncates if > 65535
```
**Impact:**
Ticks longer than ~65ms will have incorrect timing metrics, affecting performance analysis.
**Recommended Fix:**
```rust
report.tick_time_us = (tick_end - tick_start).min(u16::MAX as u32) as u16;
```
---
### CGK-014: Silent JSON Serialization Failure
**Severity:** Low
**Location:** `/home/user/ruvector/crates/cognitum-gate-tilezero/src/receipt.rs:82-83`
**CVSS:** 3.1
**Description:**
```rust
pub fn hash(&self) -> [u8; 32] {
let json = serde_json::to_vec(self).unwrap_or_default(); // Silent failure!
*blake3::hash(&json).as_bytes()
}
```
**Impact:**
If serialization fails, an empty hash is computed, potentially causing hash collisions.
**Recommended Fix:**
```rust
pub fn hash(&self) -> Result<[u8; 32], HashError> {
let json = serde_json::to_vec(self)?;
Ok(*blake3::hash(&json).as_bytes())
}
```
---
### CGK-015: Information Disclosure in Error Messages
**Severity:** Low
**Location:** `/home/user/ruvector/crates/mcp-gate/src/tools.rs:292-355`
**CVSS:** 3.0
**Description:**
Error messages expose internal state details:
```rust
format!("Min-cut {:.3} below threshold {:.3}", mincut_value, self.thresholds.min_cut)
format!("E-value {:.4} indicates strong evidence of incoherence", summary.evidential.e_value)
```
**Impact:**
Exposes exact threshold values and internal metrics to clients, aiding targeted attacks.
**Recommended Fix:**
Return generic error codes to external clients; log detailed messages internally only.
---
### CGK-016: No Input Size Limits on Tool Calls
**Severity:** Low
**Location:** `/home/user/ruvector/crates/mcp-gate/src/tools.rs:126-159`
**CVSS:** 2.8
**Description:**
The `call_tool` function deserializes JSON without size limits:
```rust
let request: PermitActionRequest = serde_json::from_value(call.arguments)
.map_err(|e| McpError::InvalidRequest(e.to_string()))?;
```
**Impact:**
Very large JSON payloads could cause memory exhaustion.
**Recommended Fix:**
Add a size limit check before deserialization or use `serde_json` with size limits.
---
### CGK-017: Hardcoded Escalation Timeout
**Severity:** Low
**Location:** `/home/user/ruvector/crates/mcp-gate/src/tools.rs:194`
**CVSS:** 2.5
**Description:**
```rust
timeout_ns: 300_000_000_000, // 5 minutes - hardcoded
```
**Impact:**
Cannot adjust escalation timeout without code changes; not a direct security issue but affects operational security.
**Recommended Fix:**
Make configurable via `GateThresholds` or environment variable.
---
## Recommendations Summary
### Immediate Actions (Critical/High)
1. **Fix signature verification** (CGK-001, CGK-002) - This is a complete authentication bypass
2. **Add synchronization to global state** (CGK-003, CGK-005) - Prevents data races
3. **Add alignment/null checks to raw pointer operations** (CGK-004)
4. **Add validation to delta processing** (CGK-006)
### Short-term Actions (Medium)
5. **Validate thresholds before computing reciprocals** (CGK-007)
6. **Use checked arithmetic for token TTL** (CGK-008)
7. **Bound receipt log size and optimize chain verification** (CGK-009)
8. **Reorder bit masking in evidence window** (CGK-010)
9. **Handle system time edge cases** (CGK-011)
### Long-term Actions (Low)
10. **Sanitize error messages for external clients** (CGK-015)
11. **Add input size limits** (CGK-016)
12. **Make operational parameters configurable** (CGK-017)
---
## Unsafe Code Audit Summary
| File | Unsafe Blocks | Safety Concerns |
|------|---------------|-----------------|
| kernel/lib.rs | 8 | Global state, raw pointers, union access |
| kernel/shard.rs | 14 | Unchecked array indexing (performance-critical) |
| kernel/evidence.rs | 4 | Unchecked window access |
| kernel/report.rs | 0 | None |
| tilezero/lib.rs | 0 | None |
| tilezero/permit.rs | 0 | None (but cryptographic issues) |
| tilezero/receipt.rs | 0 | None |
| tilezero/decision.rs | 0 | None |
| mcp-gate/tools.rs | 0 | None |
The kernel crate uses unsafe code extensively for performance optimization. Each instance should be audited against its safety invariants.
---
## Testing Recommendations
1. **Fuzzing:** Apply `cargo-fuzz` to delta parsing and token decoding
2. **Property testing:** Use `proptest` for invariant validation
3. **Miri:** Run `cargo miri test` to detect undefined behavior
4. **Memory sanitizers:** Test with AddressSanitizer and MemorySanitizer
---
## Compliance Notes
- **No timing attacks identified** in the cryptographic code (uses constant-time libraries)
- **Key generation** uses `OsRng` which is cryptographically secure
- **Hash function** (blake3) is modern and appropriate
- **Signature scheme** (Ed25519) is appropriate but implementation is broken
---
## Appendix A: Delta Module Analysis
**File:** `/home/user/ruvector/crates/cognitum-gate-kernel/src/delta.rs`
The delta module implements a tagged union (`DeltaPayload`) for graph updates. The design is sound but has some security considerations:
### Union Safety
The `DeltaPayload` union is correctly sized (8 bytes for all variants) with compile-time assertions. The unsafe accessor methods (`get_edge_add`, `get_edge_remove`, etc.) correctly require the caller to verify the tag before access.
**Current Implementation (Lines 379-401):**
```rust
/// Get the edge add payload (unsafe: caller must verify tag)
pub unsafe fn get_edge_add(&self) -> &EdgeAdd {
unsafe { &self.payload.edge_add }
}
```
**Recommendation:** Consider adding debug assertions:
```rust
#[inline]
pub unsafe fn get_edge_add(&self) -> &EdgeAdd {
debug_assert_eq!(self.tag, DeltaTag::EdgeAdd, "Invalid tag for EdgeAdd access");
unsafe { &self.payload.edge_add }
}
```
### Alignment Considerations
The `Delta` struct is aligned to 16 bytes (`#[repr(C, align(16))]`), which is correct for WASM and most architectures. However, when deserializing from raw bytes (as in `ingest_delta_raw`), alignment must be verified.
---
## Appendix B: Threat Model Summary
| Threat | Likelihood | Impact | Mitigation Status |
|--------|------------|--------|------------------|
| Token forgery (CGK-001/002) | High | Critical | NOT MITIGATED |
| Memory corruption via malformed delta | Medium | High | Partial (tag check, no integrity check) |
| DoS via memory exhaustion | Medium | Medium | Partial (fixed buffers, but unbounded log) |
| Race condition exploitation | Low | High | NOT MITIGATED (single-threaded WASM assumed) |
| Timing side-channel | Low | Low | Mitigated (constant-time crypto libs) |
---
## Appendix C: Verification Status of Unsafe Code Invariants
| Location | Invariant | Verified By |
|----------|-----------|-------------|
| shard.rs:450 | source < MAX_SHARD_VERTICES | Bounds check at line 445 |
| shard.rs:457 | degree <= MAX_DEGREE | Struct invariant (add_edge checks) |
| shard.rs:576-577 | root < MAX_SHARD_VERTICES | Loop construction |
| evidence.rs:410 | idx < WINDOW_SIZE | **BROKEN** - mask applied after access |
| lib.rs:208 | ptr aligned to Delta alignment | **NOT VERIFIED** |
| lib.rs:292 | tag matches payload variant | Tag set during construction only |
---
*Report generated by Claude Code Security Review Agent*
*Classification: Internal Security Document*