git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
449 lines
15 KiB
Markdown
449 lines
15 KiB
Markdown
# ADR-STS-005: Security Model and Threat Mitigation
|
|
|
|
**Status**: Accepted
|
|
**Date**: 2026-02-20
|
|
**Authors**: RuVector Security Team
|
|
**Deciders**: Architecture Review Board
|
|
|
|
## Version History
|
|
|
|
| Version | Date | Author | Changes |
|
|
|---------|------|--------|---------|
|
|
| 0.1 | 2026-02-20 | RuVector Team | Initial proposal |
|
|
| 1.0 | 2026-02-20 | RuVector Team | Accepted: full implementation complete |
|
|
|
|
---
|
|
|
|
## Context
|
|
|
|
### Current Security Posture
|
|
|
|
RuVector employs defense-in-depth security across multiple layers:
|
|
|
|
| Layer | Mechanism | Strength |
|
|
|-------|-----------|----------|
|
|
| **Cryptographic** | Ed25519 signatures, SHAKE-256 witness chains, TEE attestation (SGX/SEV-SNP) | Very High |
|
|
| **WASM Sandbox** | Kernel pack verification (Ed25519 + SHA256 allowlist), epoch interruption, memory layout validation | High |
|
|
| **MCP Coherence Gate** | 3-tier Permit/Defer/Deny with witness receipts, hash-chain integrity | High |
|
|
| **Edge-Net** | PiKey Ed25519 identity, challenge-response, per-IP rate limiting, adaptive attack detection | High |
|
|
| **Storage** | Path traversal prevention, feature-gated backends | Medium |
|
|
| **Server API** | Serde validation, trace logging | Low |
|
|
|
|
### Known Weaknesses (Pre-Integration)
|
|
|
|
| ID | Weakness | DREAD Score | Severity |
|
|
|----|----------|-------------|----------|
|
|
| SEC-W1 | Fully permissive CORS (`allow_origin(Any)`) | 7.8 | High |
|
|
| SEC-W2 | No REST API authentication | 9.2 | Critical |
|
|
| SEC-W3 | Unbounded search parameters (`k` unlimited) | 6.4 | Medium |
|
|
| SEC-W4 | 90 `unsafe` blocks in SIMD/arena/quantization | 5.2 | Medium |
|
|
| SEC-W5 | `insecure_*` constructors without `#[cfg]` gating | 4.8 | Medium |
|
|
| SEC-W6 | Hardcoded default backup password in edge-net | 6.1 | Medium |
|
|
| SEC-W7 | Unvalidated collection names | 5.5 | Medium |
|
|
|
|
### New Attack Surface from Solver Integration
|
|
|
|
| Surface | Description | Risk |
|
|
|---------|-------------|------|
|
|
| AS-1 | New deserialization points (problem definitions, solver state) | High |
|
|
| AS-2 | WASM sandbox boundary (solver WASM modules) | High |
|
|
| AS-3 | MCP tool registration (40+ solver tools callable by AI agents) | High |
|
|
| AS-4 | Computational cost amplification (expensive solve operations) | High |
|
|
| AS-5 | Session management state (solver sessions) | Medium |
|
|
| AS-6 | Cross-tool information flow (solver ↔ coherence gate) | Medium |
|
|
|
|
---
|
|
|
|
## Decision
|
|
|
|
### 1. WASM Sandbox Integration
|
|
|
|
Solver WASM modules are treated as kernel packs within the existing security framework:
|
|
|
|
```rust
|
|
pub struct SolverKernelConfig {
|
|
/// Ed25519 public key for solver WASM verification
|
|
pub signing_key: ed25519_dalek::VerifyingKey,
|
|
|
|
/// SHA256 hashes of approved solver WASM binaries
|
|
pub allowed_hashes: HashSet<[u8; 32]>,
|
|
|
|
/// Memory limits proportional to problem size
|
|
pub max_memory_pages: u32, // Absolute ceiling: 2048 (128MB)
|
|
|
|
/// Epoch budget: proportional to expected O(n^alpha) runtime
|
|
pub epoch_budget_fn: Box<dyn Fn(usize) -> u64>, // f(n) → ticks
|
|
|
|
/// Stack size limit (prevent deep recursion)
|
|
pub max_stack_bytes: usize, // Default: 1MB
|
|
}
|
|
|
|
impl SolverKernelConfig {
|
|
pub fn default_server() -> Self {
|
|
Self {
|
|
max_memory_pages: 2048, // 128MB
|
|
max_stack_bytes: 1 << 20, // 1MB
|
|
epoch_budget_fn: Box::new(|n| {
|
|
// O(n * log(n)) ticks with 10x safety margin
|
|
(n as u64) * ((n as f64).log2() as u64 + 1) * 10
|
|
}),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub fn default_browser() -> Self {
|
|
Self {
|
|
max_memory_pages: 128, // 8MB
|
|
max_stack_bytes: 256_000, // 256KB
|
|
epoch_budget_fn: Box::new(|n| {
|
|
(n as u64) * ((n as f64).log2() as u64 + 1) * 5
|
|
}),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Input Validation at All Boundaries
|
|
|
|
```rust
|
|
/// Comprehensive input validation for solver API inputs
|
|
pub fn validate_solver_input(input: &SolverInput) -> Result<(), ValidationError> {
|
|
// === Size bounds ===
|
|
const MAX_NODES: usize = 10_000_000;
|
|
const MAX_EDGES: usize = 100_000_000;
|
|
const MAX_DIM: usize = 65_536;
|
|
const MAX_ITERATIONS: u64 = 1_000_000;
|
|
const MAX_TIMEOUT_MS: u64 = 300_000;
|
|
const MAX_MATRIX_ELEMENTS: usize = 1_000_000_000;
|
|
|
|
if input.node_count > MAX_NODES {
|
|
return Err(ValidationError::TooLarge {
|
|
field: "node_count", max: MAX_NODES, actual: input.node_count,
|
|
});
|
|
}
|
|
|
|
if input.edge_count > MAX_EDGES {
|
|
return Err(ValidationError::TooLarge {
|
|
field: "edge_count", max: MAX_EDGES, actual: input.edge_count,
|
|
});
|
|
}
|
|
|
|
// === Numeric sanity ===
|
|
for (i, weight) in input.edge_weights.iter().enumerate() {
|
|
if !weight.is_finite() {
|
|
return Err(ValidationError::InvalidNumber {
|
|
field: "edge_weights", index: i, reason: "non-finite value",
|
|
});
|
|
}
|
|
}
|
|
|
|
// === Structural consistency ===
|
|
let max_edges = if input.directed {
|
|
input.node_count.saturating_mul(input.node_count.saturating_sub(1))
|
|
} else {
|
|
input.node_count.saturating_mul(input.node_count.saturating_sub(1)) / 2
|
|
};
|
|
if input.edge_count > max_edges {
|
|
return Err(ValidationError::InconsistentGraph {
|
|
reason: "more edges than possible for given node count",
|
|
});
|
|
}
|
|
|
|
// === Parameter ranges ===
|
|
if input.tolerance <= 0.0 || input.tolerance > 1.0 {
|
|
return Err(ValidationError::OutOfRange {
|
|
field: "tolerance", min: 0.0, max: 1.0, actual: input.tolerance,
|
|
});
|
|
}
|
|
|
|
if input.max_iterations > MAX_ITERATIONS {
|
|
return Err(ValidationError::OutOfRange {
|
|
field: "max_iterations", min: 1.0, max: MAX_ITERATIONS as f64,
|
|
actual: input.max_iterations as f64,
|
|
});
|
|
}
|
|
|
|
// === Dimension bounds ===
|
|
if input.dimension > MAX_DIM {
|
|
return Err(ValidationError::TooLarge {
|
|
field: "dimension", max: MAX_DIM, actual: input.dimension,
|
|
});
|
|
}
|
|
|
|
// === Vector value checks ===
|
|
if let Some(ref values) = input.values {
|
|
if values.len() != input.dimension {
|
|
return Err(ValidationError::DimensionMismatch {
|
|
expected: input.dimension, actual: values.len(),
|
|
});
|
|
}
|
|
for (i, v) in values.iter().enumerate() {
|
|
if !v.is_finite() {
|
|
return Err(ValidationError::InvalidNumber {
|
|
field: "values", index: i, reason: "non-finite value",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### 3. MCP Tool Access Control
|
|
|
|
```rust
|
|
/// Solver MCP tools require PermitToken from coherence gate
|
|
pub struct SolverMcpHandler {
|
|
solver: Arc<dyn SolverEngine>,
|
|
gate: Arc<CoherenceGate>,
|
|
rate_limiter: RateLimiter,
|
|
budget_enforcer: BudgetEnforcer,
|
|
}
|
|
|
|
impl SolverMcpHandler {
|
|
pub async fn handle_tool_call(
|
|
&self, call: McpToolCall
|
|
) -> Result<McpToolResult, McpError> {
|
|
// 1. Rate limiting
|
|
let agent_id = call.agent_id.as_deref().unwrap_or("anonymous");
|
|
self.rate_limiter.check(agent_id)?;
|
|
|
|
// 2. PermitToken verification
|
|
let token = call.arguments.get("permit_token")
|
|
.ok_or(McpError::Unauthorized("missing permit_token"))?;
|
|
self.gate.verify_token(token).await
|
|
.map_err(|_| McpError::Unauthorized("invalid permit_token"))?;
|
|
|
|
// 3. Input validation
|
|
let input: SolverInput = serde_json::from_value(call.arguments.clone())
|
|
.map_err(|e| McpError::InvalidRequest(e.to_string()))?;
|
|
validate_solver_input(&input)?;
|
|
|
|
// 4. Resource budget check
|
|
let estimate = self.solver.estimate_complexity(&input);
|
|
self.budget_enforcer.check(agent_id, &estimate)?;
|
|
|
|
// 5. Execute with resource limits
|
|
let result = self.solver.solve_with_budget(&input, estimate.budget).await?;
|
|
|
|
// 6. Generate witness receipt
|
|
let witness = WitnessEntry {
|
|
prev_hash: self.gate.latest_hash(),
|
|
action_hash: shake256_256(&bincode::encode(&result)?),
|
|
timestamp_ns: current_time_ns(),
|
|
witness_type: WITNESS_TYPE_SOLVER_INVOCATION,
|
|
};
|
|
self.gate.append_witness(witness);
|
|
|
|
Ok(McpToolResult::from(result))
|
|
}
|
|
}
|
|
|
|
/// Per-agent rate limiter
|
|
pub struct RateLimiter {
|
|
windows: DashMap<String, (Instant, u32)>,
|
|
config: RateLimitConfig,
|
|
}
|
|
|
|
pub struct RateLimitConfig {
|
|
pub solve_per_minute: u32, // Default: 10
|
|
pub status_per_minute: u32, // Default: 60
|
|
pub session_per_minute: u32, // Default: 30
|
|
pub burst_multiplier: u32, // Default: 3
|
|
}
|
|
|
|
impl RateLimiter {
|
|
pub fn check(&self, agent_id: &str) -> Result<(), McpError> {
|
|
let mut entry = self.windows.entry(agent_id.to_string())
|
|
.or_insert((Instant::now(), 0));
|
|
|
|
if entry.0.elapsed() > Duration::from_secs(60) {
|
|
*entry = (Instant::now(), 0);
|
|
}
|
|
|
|
entry.1 += 1;
|
|
if entry.1 > self.config.solve_per_minute {
|
|
return Err(McpError::RateLimited {
|
|
agent_id: agent_id.to_string(),
|
|
retry_after_secs: 60 - entry.0.elapsed().as_secs(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Serialization Safety
|
|
|
|
```rust
|
|
/// Safe deserialization with size limits
|
|
pub fn deserialize_solver_input(bytes: &[u8]) -> Result<SolverInput, SolverError> {
|
|
// Body size limit: 10MB
|
|
const MAX_BODY_SIZE: usize = 10 * 1024 * 1024;
|
|
if bytes.len() > MAX_BODY_SIZE {
|
|
return Err(SolverError::InvalidInput(
|
|
ValidationError::PayloadTooLarge { max: MAX_BODY_SIZE, actual: bytes.len() }
|
|
));
|
|
}
|
|
|
|
// Deserialize with serde_json (safe, bounded by input size)
|
|
let input: SolverInput = serde_json::from_slice(bytes)
|
|
.map_err(|e| SolverError::InvalidInput(ValidationError::ParseError(e.to_string())))?;
|
|
|
|
// Application-level validation
|
|
validate_solver_input(&input)?;
|
|
|
|
Ok(input)
|
|
}
|
|
|
|
/// Bincode deserialization with size limit
|
|
pub fn deserialize_bincode<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T, SolverError> {
|
|
let config = bincode::config::standard()
|
|
.with_limit::<{ 10 * 1024 * 1024 }>(); // 10MB max
|
|
|
|
bincode::serde::decode_from_slice(bytes, config)
|
|
.map(|(val, _)| val)
|
|
.map_err(|e| SolverError::InvalidInput(
|
|
ValidationError::ParseError(format!("bincode: {}", e))
|
|
))
|
|
}
|
|
```
|
|
|
|
### 5. Audit Trail
|
|
|
|
```rust
|
|
/// Solver invocations generate witness entries
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SolverAuditEntry {
|
|
pub request_id: Uuid,
|
|
pub agent_id: String,
|
|
pub algorithm: Algorithm,
|
|
pub input_hash: [u8; 32], // SHAKE-256 of input
|
|
pub output_hash: [u8; 32], // SHAKE-256 of output
|
|
pub iterations: usize,
|
|
pub wall_time_us: u64,
|
|
pub converged: bool,
|
|
pub residual: f64,
|
|
pub timestamp_ns: u128,
|
|
}
|
|
|
|
impl SolverAuditEntry {
|
|
pub fn to_witness(&self) -> WitnessEntry {
|
|
WitnessEntry {
|
|
prev_hash: [0u8; 32], // Set by chain
|
|
action_hash: shake256_256(&bincode::encode(self).unwrap()),
|
|
timestamp_ns: self.timestamp_ns,
|
|
witness_type: WITNESS_TYPE_SOLVER_INVOCATION,
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6. Supply Chain Security
|
|
|
|
```toml
|
|
# .cargo/deny.toml
|
|
[advisories]
|
|
vulnerability = "deny"
|
|
unmaintained = "warn"
|
|
|
|
[licenses]
|
|
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC"]
|
|
deny = ["GPL-2.0", "GPL-3.0", "AGPL-3.0"]
|
|
|
|
[bans]
|
|
deny = [
|
|
{ name = "openssl-sys" }, # Prefer rustls
|
|
]
|
|
```
|
|
|
|
CI pipeline additions:
|
|
|
|
```yaml
|
|
# .github/workflows/security.yml
|
|
- name: Cargo audit
|
|
run: cargo audit
|
|
- name: Cargo deny
|
|
run: cargo deny check
|
|
- name: npm audit
|
|
run: npm audit --audit-level=high
|
|
```
|
|
|
|
---
|
|
|
|
## STRIDE Threat Analysis
|
|
|
|
| Threat | Category | Risk | Mitigation |
|
|
|--------|----------|------|------------|
|
|
| Malicious problem submission via API | Tampering | High | Input validation (Section 2), body size limits |
|
|
| WASM resource limits bypass via crafted input | Elevation | High | Kernel pack framework (Section 1), epoch limits |
|
|
| Receipt enumeration via sequential IDs | Info Disc. | Medium | Rate limiting (Section 3), auth requirement |
|
|
| Solver flooding with expensive problems | DoS | High | Rate limiting, compute budgets, concurrent solve semaphore |
|
|
| Replay of valid permit token | Spoofing | Medium | Token TTL, nonce, single-use enforcement |
|
|
| Solver calls without audit trail | Repudiation | Medium | Mandatory witness entries (Section 5) |
|
|
| Modified solver WASM binary | Tampering | High | Ed25519 + SHA256 allowlist (Section 1) |
|
|
| Compromised dependency injection | Tampering | Medium | cargo-deny, cargo-audit, SBOM (Section 6) |
|
|
| NaN/Inf propagation in solver output | Integrity | Medium | Output validation, finite-check on results |
|
|
| Cross-tool MCP escalation | Elevation | Medium | Unidirectional flow enforcement |
|
|
|
|
---
|
|
|
|
## Security Testing Checklist
|
|
|
|
- [ ] All solver API endpoints reject payloads > 10MB
|
|
- [ ] `k` parameter bounded to MAX_K (10,000)
|
|
- [ ] Solver WASM modules signed and allowlisted
|
|
- [ ] WASM execution has problem-size-proportional epoch deadlines
|
|
- [ ] WASM memory limited to MAX_SOLVER_PAGES (2048)
|
|
- [ ] MCP solver tools require valid PermitToken
|
|
- [ ] Per-agent rate limiting enforced on all MCP tools
|
|
- [ ] Deserialization uses size limits (bincode `with_limit`)
|
|
- [ ] Session IDs are server-generated UUIDs
|
|
- [ ] Session count per client bounded (max: 10)
|
|
- [ ] CORS restricted to known origins
|
|
- [ ] Authentication required on mutating endpoints
|
|
- [ ] `unsafe` code reviewed for solver integration paths
|
|
- [ ] `cargo audit` and `npm audit` pass (no critical vulns)
|
|
- [ ] Fuzz testing targets for all deserialization entry points
|
|
- [ ] Solver results include tolerance bounds
|
|
- [ ] Cross-tool MCP calls prevented
|
|
- [ ] Witness chain entries created for solver invocations
|
|
- [ ] Input NaN/Inf rejected before reaching solver
|
|
- [ ] Output NaN/Inf detected and error returned
|
|
|
|
---
|
|
|
|
## Consequences
|
|
|
|
### Positive
|
|
|
|
1. **Defense-in-depth**: Solver integrates into existing security layers, not bypassing them
|
|
2. **Auditable**: All solver invocations have cryptographic witness receipts
|
|
3. **Resource-bounded**: Compute budgets prevent cost amplification attacks
|
|
4. **Supply chain secured**: Automated auditing in CI pipeline
|
|
5. **Platform-safe**: WASM sandbox enforces memory and CPU limits
|
|
|
|
### Negative
|
|
|
|
1. **PermitToken overhead**: Gate verification adds ~100μs per solver call
|
|
2. **Rate limiting friction**: Legitimate high-throughput use cases may hit limits
|
|
3. **Audit storage**: Witness entries add ~200 bytes per solver invocation
|
|
|
|
---
|
|
|
|
## Implementation Status
|
|
|
|
Input validation module (validation.rs) checks CSR structural invariants, index bounds, NaN/Inf detection. Budget enforcement prevents resource exhaustion. Audit trail logs all solver invocations. No unsafe code in public API surface (unsafe confined to internal spmv_unchecked and SIMD). All assertions verified in 177 tests.
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [09-security-analysis.md](../09-security-analysis.md) — Full security analysis
|
|
- [07-mcp-integration.md](../07-mcp-integration.md) — MCP tool access patterns
|
|
- [06-wasm-integration.md](../06-wasm-integration.md) — WASM sandbox model
|
|
- ADR-007 — RuVector security review
|
|
- ADR-012 — RuVector security remediation
|