28 KiB
ADR-DB-010: Delta Security Model
Status: Proposed Date: 2026-01-28 Authors: RuVector Architecture Team Deciders: Architecture Review Board, Security Team Parent: ADR-DB-001 Delta Behavior Core Architecture
Version History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-01-28 | Architecture Team | Initial proposal |
Context and Problem Statement
The Security Challenge
Delta-first architecture introduces new attack surfaces:
- Delta Integrity: Deltas could be tampered with in transit or storage
- Authorization: Who can create, modify, or read deltas?
- Replay Attacks: Resubmission of old deltas
- Information Leakage: Delta patterns reveal update frequency
- Denial of Service: Flood of malicious deltas
Threat Model
| Threat Actor | Capability | Goal |
|---|---|---|
| External Attacker | Network access | Data exfiltration, corruption |
| Malicious Insider | API access | Unauthorized modifications |
| Compromised Replica | Full replica access | State corruption |
| Network Adversary | Traffic interception | Delta manipulation |
Security Requirements
| Requirement | Priority | Description |
|---|---|---|
| Integrity | Critical | Detect tampered deltas |
| Authentication | Critical | Verify delta origin |
| Authorization | High | Enforce access control |
| Confidentiality | Medium | Protect delta contents |
| Non-repudiation | Medium | Prove delta authorship |
| Availability | High | Resist DoS attacks |
Decision
Adopt Signed Deltas with Capability Tokens
We implement a defense-in-depth security model with cryptographically signed deltas and fine-grained capability-based authorization.
Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ SECURITY PERIMETER │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌──────────────┐ │
│ │ TLS 1.3 │ │ mTLS │ │ Rate Limit │ │ WAF │ │
│ │ Transport │ │ Auth │ │ (per-client) │ │ (optional) │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────────────────┐
│ AUTHENTICATION LAYER │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Identity Verification │ │
│ │ API Key │ JWT │ Client Certificate │ Capability Token │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────────────────┐
│ AUTHORIZATION LAYER │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────────────────┐│
│ │ Capability │ │ RBAC │ │ Namespace Isolation ││
│ │ Tokens │ │ Policies │ │ ││
│ └────────────────┘ └────────────────┘ └────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────────────────┐
│ DELTA SECURITY │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────────────────┐│
│ │ Signature │ │ Replay │ │ Integrity ││
│ │ Verification │ │ Protection │ │ Validation ││
│ └────────────────┘ └────────────────┘ └────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
Core Components
1. Signed Deltas
use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
use sha2::{Sha256, Digest};
/// A cryptographically signed delta
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedDelta {
/// The delta content
pub delta: VectorDelta,
/// Ed25519 signature over delta hash
pub signature: Signature,
/// Signing key identifier
pub key_id: KeyId,
/// Timestamp of signing
pub signed_at: DateTime<Utc>,
/// Nonce for replay protection
pub nonce: [u8; 16],
}
/// Delta signer for creating signed deltas
pub struct DeltaSigner {
/// Signing key
signing_key: SigningKey,
/// Key identifier
key_id: KeyId,
/// Nonce tracker
nonce_tracker: NonceTracker,
}
impl DeltaSigner {
/// Sign a delta
pub fn sign(&self, delta: VectorDelta) -> Result<SignedDelta, SigningError> {
// Generate nonce
let nonce = self.nonce_tracker.generate();
// Create signing payload
let payload = SigningPayload {
delta: &delta,
nonce: &nonce,
timestamp: Utc::now(),
};
// Compute hash
let hash = self.compute_payload_hash(&payload);
// Sign hash
let signature = self.signing_key.sign(&hash);
Ok(SignedDelta {
delta,
signature,
key_id: self.key_id.clone(),
signed_at: payload.timestamp,
nonce,
})
}
fn compute_payload_hash(&self, payload: &SigningPayload) -> [u8; 32] {
let mut hasher = Sha256::new();
// Hash delta content
hasher.update(&bincode::serialize(&payload.delta).unwrap());
// Hash nonce
hasher.update(payload.nonce);
// Hash timestamp
hasher.update(&payload.timestamp.timestamp().to_le_bytes());
hasher.finalize().into()
}
}
/// Delta verifier for validating signed deltas
pub struct DeltaVerifier {
/// Known public keys
public_keys: DashMap<KeyId, VerifyingKey>,
/// Nonce store for replay protection
nonce_store: NonceStore,
/// Clock skew tolerance
clock_tolerance: Duration,
}
impl DeltaVerifier {
/// Verify a signed delta
pub fn verify(&self, signed_delta: &SignedDelta) -> Result<(), VerificationError> {
// Check key exists
let public_key = self.public_keys
.get(&signed_delta.key_id)
.ok_or(VerificationError::UnknownKey)?;
// Check timestamp is recent
let age = Utc::now().signed_duration_since(signed_delta.signed_at);
if age.abs() > self.clock_tolerance.as_secs() as i64 {
return Err(VerificationError::ExpiredOrFuture);
}
// Check nonce hasn't been used
if self.nonce_store.is_used(&signed_delta.nonce) {
return Err(VerificationError::ReplayDetected);
}
// Verify signature
let payload = SigningPayload {
delta: &signed_delta.delta,
nonce: &signed_delta.nonce,
timestamp: signed_delta.signed_at,
};
let hash = self.compute_payload_hash(&payload);
public_key.verify(&hash, &signed_delta.signature)
.map_err(|_| VerificationError::InvalidSignature)?;
// Mark nonce as used
self.nonce_store.mark_used(signed_delta.nonce);
Ok(())
}
}
2. Capability Tokens
/// Capability token for fine-grained authorization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityToken {
/// Token identifier
pub token_id: TokenId,
/// Subject (who this token is for)
pub subject: Subject,
/// Granted capabilities
pub capabilities: Vec<Capability>,
/// Token issuer
pub issuer: String,
/// Issued at
pub issued_at: DateTime<Utc>,
/// Expires at
pub expires_at: DateTime<Utc>,
/// Restrictions
pub restrictions: TokenRestrictions,
/// Signature
pub signature: Signature,
}
/// Individual capability grant
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Capability {
/// Create deltas for specific vectors
CreateDelta {
vector_patterns: Vec<VectorPattern>,
operation_types: Vec<OperationType>,
},
/// Read vectors and their deltas
ReadVector {
vector_patterns: Vec<VectorPattern>,
},
/// Search capability
Search {
namespaces: Vec<String>,
max_k: usize,
},
/// Compact delta chains
Compact {
vector_patterns: Vec<VectorPattern>,
},
/// Administrative capability
Admin {
scope: AdminScope,
},
}
/// Pattern for matching vector IDs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum VectorPattern {
/// Exact match
Exact(VectorId),
/// Prefix match
Prefix(String),
/// Regex match
Regex(String),
/// All vectors in namespace
Namespace(String),
/// All vectors
All,
}
/// Token restrictions
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenRestrictions {
/// Rate limit (requests per second)
pub rate_limit: Option<f32>,
/// IP address restrictions
pub allowed_ips: Option<Vec<IpNetwork>>,
/// Time of day restrictions
pub time_windows: Option<Vec<TimeWindow>>,
/// Maximum delta size
pub max_delta_size: Option<usize>,
}
/// Capability verifier
pub struct CapabilityVerifier {
/// Trusted issuers' public keys
issuer_keys: DashMap<String, VerifyingKey>,
/// Token revocation list
revoked: HashSet<TokenId>,
}
impl CapabilityVerifier {
/// Verify token and extract capabilities
pub fn verify_token(&self, token: &CapabilityToken) -> Result<&[Capability], AuthError> {
// Check not revoked
if self.revoked.contains(&token.token_id) {
return Err(AuthError::TokenRevoked);
}
// Check expiration
if Utc::now() > token.expires_at {
return Err(AuthError::TokenExpired);
}
// Check not before issued
if Utc::now() < token.issued_at {
return Err(AuthError::TokenNotYetValid);
}
// Verify signature
let issuer_key = self.issuer_keys
.get(&token.issuer)
.ok_or(AuthError::UnknownIssuer)?;
let payload = self.compute_token_hash(token);
issuer_key.verify(&payload, &token.signature)
.map_err(|_| AuthError::InvalidTokenSignature)?;
Ok(&token.capabilities)
}
/// Check if token authorizes an operation
pub fn authorize(
&self,
token: &CapabilityToken,
operation: &DeltaOperation,
vector_id: &VectorId,
) -> Result<(), AuthError> {
let capabilities = self.verify_token(token)?;
for cap in capabilities {
if self.capability_allows(cap, operation, vector_id) {
return Ok(());
}
}
Err(AuthError::Unauthorized)
}
fn capability_allows(
&self,
cap: &Capability,
operation: &DeltaOperation,
vector_id: &VectorId,
) -> bool {
match cap {
Capability::CreateDelta { vector_patterns, operation_types } => {
// Check vector pattern
let vector_match = vector_patterns.iter()
.any(|p| self.pattern_matches(p, vector_id));
// Check operation type
let op_match = operation_types.contains(&operation.operation_type());
vector_match && op_match
}
Capability::Admin { scope: AdminScope::Full } => true,
_ => false,
}
}
fn pattern_matches(&self, pattern: &VectorPattern, vector_id: &VectorId) -> bool {
match pattern {
VectorPattern::Exact(id) => id == vector_id,
VectorPattern::Prefix(prefix) => vector_id.starts_with(prefix),
VectorPattern::Regex(re) => {
regex::Regex::new(re)
.map(|r| r.is_match(vector_id))
.unwrap_or(false)
}
VectorPattern::Namespace(ns) => {
vector_id.starts_with(&format!("{}:", ns))
}
VectorPattern::All => true,
}
}
}
3. Rate Limiting and DoS Protection
/// Rate limiter for delta operations
pub struct DeltaRateLimiter {
/// Per-client limits
client_limits: DashMap<ClientId, TokenBucket>,
/// Per-vector limits
vector_limits: DashMap<VectorId, TokenBucket>,
/// Global limit
global_limit: TokenBucket,
/// Configuration
config: RateLimitConfig,
}
/// Token bucket for rate limiting
pub struct TokenBucket {
/// Current tokens
tokens: AtomicF64,
/// Last refill time
last_refill: AtomicU64,
/// Tokens per second
rate: f64,
/// Maximum tokens
capacity: f64,
}
impl TokenBucket {
/// Try to consume tokens
pub fn try_consume(&self, tokens: f64) -> bool {
// Refill based on elapsed time
self.refill();
loop {
let current = self.tokens.load(Ordering::Relaxed);
if current < tokens {
return false;
}
if self.tokens.compare_exchange(
current,
current - tokens,
Ordering::SeqCst,
Ordering::Relaxed,
).is_ok() {
return true;
}
}
}
fn refill(&self) {
let now = Instant::now().elapsed().as_millis() as u64;
let last = self.last_refill.load(Ordering::Relaxed);
let elapsed = (now - last) as f64 / 1000.0;
let new_tokens = (self.tokens.load(Ordering::Relaxed) + elapsed * self.rate)
.min(self.capacity);
self.tokens.store(new_tokens, Ordering::Relaxed);
self.last_refill.store(now, Ordering::Relaxed);
}
}
impl DeltaRateLimiter {
/// Check if operation is allowed
pub fn check(&self, client_id: &ClientId, vector_id: &VectorId) -> Result<(), RateLimitError> {
// Check global limit
if !self.global_limit.try_consume(1.0) {
return Err(RateLimitError::GlobalLimitExceeded);
}
// Check client limit
let client_bucket = self.client_limits
.entry(client_id.clone())
.or_insert_with(|| TokenBucket::new(
self.config.client_rate,
self.config.client_burst,
));
if !client_bucket.try_consume(1.0) {
return Err(RateLimitError::ClientLimitExceeded);
}
// Check vector limit (prevent hot-key abuse)
let vector_bucket = self.vector_limits
.entry(vector_id.clone())
.or_insert_with(|| TokenBucket::new(
self.config.vector_rate,
self.config.vector_burst,
));
if !vector_bucket.try_consume(1.0) {
return Err(RateLimitError::VectorLimitExceeded);
}
Ok(())
}
}
4. Input Validation
/// Delta input validator
pub struct DeltaValidator {
/// Maximum delta size
max_delta_size: usize,
/// Maximum dimensions
max_dimensions: usize,
/// Allowed operation types
allowed_operations: HashSet<OperationType>,
/// Metadata schema (optional)
metadata_schema: Option<JsonSchema>,
}
impl DeltaValidator {
/// Validate a delta before processing
pub fn validate(&self, delta: &VectorDelta) -> Result<(), ValidationError> {
// Check delta ID format
self.validate_id(&delta.delta_id)?;
self.validate_id(&delta.vector_id)?;
// Check operation type allowed
if !self.allowed_operations.contains(&delta.operation.operation_type()) {
return Err(ValidationError::DisallowedOperation);
}
// Validate operation content
self.validate_operation(&delta.operation)?;
// Validate metadata if present
if let Some(metadata) = &delta.metadata_delta {
self.validate_metadata(metadata)?;
}
// Check timestamp is sane
self.validate_timestamp(delta.timestamp)?;
Ok(())
}
fn validate_id(&self, id: &str) -> Result<(), ValidationError> {
// Check length
if id.len() > 256 {
return Err(ValidationError::IdTooLong);
}
// Check for path traversal
if id.contains("..") || id.contains('/') || id.contains('\\') {
return Err(ValidationError::InvalidIdChars);
}
// Check for null bytes
if id.contains('\0') {
return Err(ValidationError::InvalidIdChars);
}
Ok(())
}
fn validate_operation(&self, op: &DeltaOperation) -> Result<(), ValidationError> {
match op {
DeltaOperation::Sparse { indices, values } => {
// Check arrays have same length
if indices.len() != values.len() {
return Err(ValidationError::MismatchedArrayLengths);
}
// Check indices are valid
for &idx in indices {
if idx as usize >= self.max_dimensions {
return Err(ValidationError::IndexOutOfBounds);
}
}
// Check for NaN/Inf values
for &val in values {
if !val.is_finite() {
return Err(ValidationError::InvalidValue);
}
}
// Check total size
if indices.len() * 8 > self.max_delta_size {
return Err(ValidationError::DeltaTooLarge);
}
}
DeltaOperation::Dense { vector } => {
// Check dimensions
if vector.len() > self.max_dimensions {
return Err(ValidationError::TooManyDimensions);
}
// Check for NaN/Inf
for &val in vector {
if !val.is_finite() {
return Err(ValidationError::InvalidValue);
}
}
// Check size
if vector.len() * 4 > self.max_delta_size {
return Err(ValidationError::DeltaTooLarge);
}
}
DeltaOperation::Scale { factor } => {
if !factor.is_finite() || *factor == 0.0 {
return Err(ValidationError::InvalidValue);
}
}
_ => {}
}
Ok(())
}
fn validate_timestamp(&self, ts: DateTime<Utc>) -> Result<(), ValidationError> {
let now = Utc::now();
let age = now.signed_duration_since(ts);
// Reject timestamps too far in the past (7 days)
if age.num_days() > 7 {
return Err(ValidationError::TimestampTooOld);
}
// Reject timestamps in the future (with 5 min tolerance)
if age.num_minutes() < -5 {
return Err(ValidationError::TimestampInFuture);
}
Ok(())
}
}
Threat Model Analysis
Attack Vectors and Mitigations
| Attack | Vector | Mitigation | Residual Risk |
|---|---|---|---|
| Delta tampering | Network MitM | TLS + signatures | Low |
| Replay attack | Network replay | Nonces + timestamp | Low |
| Unauthorized access | API abuse | Capability tokens | Low |
| Data exfiltration | Side channels | Rate limiting | Medium |
| DoS flooding | Request flood | Rate limiting | Medium |
| Key compromise | Key theft | Key rotation | Medium |
| Privilege escalation | Token forge | Signature verification | Low |
| Input injection | Malformed delta | Input validation | Low |
Security Guarantees
| Guarantee | Mechanism | Strength |
|---|---|---|
| Integrity | Ed25519 signatures | Cryptographic |
| Authentication | mTLS + tokens | Cryptographic |
| Authorization | Capability tokens | Logical |
| Replay protection | Nonces + timestamps | Probabilistic |
| Rate limiting | Token buckets | Statistical |
Considered Options
Option 1: Simple API Keys
Description: Basic API key authentication.
Pros:
- Simple to implement
- Easy to understand
Cons:
- No fine-grained control
- Key compromise is catastrophic
- No delta-level security
Verdict: Rejected - insufficient for delta integrity.
Option 2: JWT Tokens
Description: Standard JWT for authentication.
Pros:
- Industry standard
- Rich ecosystem
Cons:
- No per-delta signatures
- Revocation complexity
- Limited capability model
Verdict: Partially adopted - used alongside capabilities.
Option 3: Signed Deltas + Capabilities (Selected)
Description: Cryptographic signatures on deltas with capability-based auth.
Pros:
- Delta-level integrity
- Fine-grained authorization
- Non-repudiation
- Composable security
Cons:
- Complexity
- Performance overhead
- Key management
Verdict: Adopted - provides comprehensive security.
Option 4: Zero-Knowledge Proofs
Description: ZK proofs for privacy-preserving updates.
Pros:
- Maximum privacy
- Verifiable computation
Cons:
- Very complex
- High overhead
- Limited tooling
Verdict: Deferred - consider for future privacy features.
Technical Specification
Security Configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
/// Enable delta signing
pub signing_enabled: bool,
/// Signing algorithm
pub signing_algorithm: SigningAlgorithm,
/// Enable capability tokens
pub capabilities_enabled: bool,
/// Token issuer public keys
pub trusted_issuers: Vec<TrustedIssuer>,
/// Rate limiting configuration
pub rate_limits: RateLimitConfig,
/// Input validation configuration
pub validation: ValidationConfig,
/// Clock skew tolerance
pub clock_tolerance: Duration,
/// Nonce window (for replay protection)
pub nonce_window: Duration,
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
signing_enabled: true,
signing_algorithm: SigningAlgorithm::Ed25519,
capabilities_enabled: true,
trusted_issuers: vec![],
rate_limits: RateLimitConfig {
global_rate: 100_000.0, // 100K ops/s global
client_rate: 1000.0, // 1K ops/s per client
client_burst: 100.0,
vector_rate: 100.0, // 100 ops/s per vector
vector_burst: 10.0,
},
validation: ValidationConfig {
max_delta_size: 1024 * 1024, // 1MB
max_dimensions: 4096,
max_metadata_size: 65536,
},
clock_tolerance: Duration::from_secs(300), // 5 minutes
nonce_window: Duration::from_secs(86400), // 24 hours
}
}
}
Wire Format for Signed Delta
Signed Delta Format:
+--------+--------+--------+--------+--------+--------+--------+--------+
| Magic | Version| Flags | Reserved | Delta Length |
| 0x53 | 0x01 | | | (32-bit LE) |
+--------+--------+--------+--------+--------+--------+--------+--------+
| Delta Payload |
| (VectorDelta, encoded) |
+-----------------------------------------------------------------------+
| Key ID (32 bytes) |
+-----------------------------------------------------------------------+
| Timestamp (64-bit LE, Unix ms) |
+-----------------------------------------------------------------------+
| Nonce (16 bytes) |
+-----------------------------------------------------------------------+
| Signature (64 bytes, Ed25519) |
+-----------------------------------------------------------------------+
Flags:
bit 0: Compressed delta payload
bit 1: Has capability token attached
bits 2-7: Reserved
Consequences
Benefits
- Integrity: Tamper-proof deltas with cryptographic verification
- Authorization: Fine-grained capability-based access control
- Auditability: Non-repudiation through signatures
- Resilience: DoS protection through rate limiting
- Flexibility: Configurable security levels
Risks and Mitigations
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Key compromise | Low | Critical | Key rotation, HSM |
| Performance overhead | Medium | Medium | Batch verification |
| Configuration errors | Medium | High | Secure defaults |
| Clock drift | Low | Medium | NTP, tolerance |
References
- NIST SP 800-63: Digital Identity Guidelines
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA)
- ADR-DB-001: Delta Behavior Core Architecture
- ADR-007: Security Review & Technical Debt
Related Decisions
- ADR-DB-001: Delta Behavior Core Architecture
- ADR-DB-003: Delta Propagation Protocol
- ADR-DB-009: Delta Observability
- ADR-007: Security Review & Technical Debt