eidetica/auth/validation/entry.rs
1//! Core entry validation for authentication
2//!
3//! This module provides the main entry point for validating entries
4//! and the AuthValidator struct that coordinates all validation operations.
5
6use std::collections::HashMap;
7
8use tracing::debug;
9
10use super::resolver::KeyResolver;
11use crate::{
12 Entry, Instance, Result,
13 auth::{
14 crypto::verify_entry_signature,
15 settings::AuthSettings,
16 types::{Operation, ResolvedAuth, SigKey},
17 },
18 constants::SETTINGS,
19};
20
21/// Authentication validator for validating entries and resolving auth information
22pub struct AuthValidator {
23 /// Cache for resolved authentication data to improve performance
24 auth_cache: HashMap<String, ResolvedAuth>,
25 /// Key resolver for handling key resolution
26 pub(crate) resolver: KeyResolver,
27}
28
29impl AuthValidator {
30 /// Create a new authentication validator
31 pub fn new() -> Self {
32 Self {
33 auth_cache: HashMap::new(),
34 resolver: KeyResolver::new(),
35 }
36 }
37
38 /// Validate an entry's authentication
39 ///
40 /// This method answers: "Is this entry valid?" which includes:
41 /// 1. Is the signature valid (or is unsigned allowed)?
42 /// 2. Does the signing key have permission for what this entry does?
43 ///
44 /// For entries with name hints that match multiple keys, this method
45 /// tries signature verification against each matching key until one succeeds.
46 ///
47 /// # Returns
48 /// - `Ok(true)` - Entry is valid (signature verified with sufficient permissions, or unsigned allowed)
49 /// - `Ok(false)` - Entry is invalid (malformed, bad signature, insufficient permissions, etc.)
50 /// - `Err(...)` - Actual error (I/O, database failures)
51 ///
52 /// # Arguments
53 /// * `entry` - The entry to validate
54 /// * `auth_settings` - Authentication settings for key lookup
55 /// * `instance` - Instance for loading delegated trees (optional for direct keys)
56 pub async fn validate_entry(
57 &mut self,
58 entry: &Entry,
59 auth_settings: &AuthSettings,
60 instance: Option<&Instance>,
61 ) -> Result<bool> {
62 // Malformed entries fail validation
63 if entry.sig.malformed_reason().is_some() {
64 debug!("Malformed entry detected");
65 return Ok(false);
66 }
67
68 // Check if auth is configured (keys or global permission)
69 let has_auth =
70 !auth_settings.get_all_keys()?.is_empty() || auth_settings.has_global_permission();
71
72 // Handle unsigned entries
73 if entry.sig.is_unsigned() {
74 if has_auth {
75 // Auth is configured but entry is unsigned - invalid
76 debug!("Unsigned entry in authenticated database");
77 return Ok(false);
78 }
79 // No auth configured, unsigned is valid
80 debug!("Unsigned entry allowed (no auth configured)");
81 return Ok(true);
82 }
83
84 // Entry is signed but no auth configured - invalid
85 if !has_auth {
86 debug!("Signed entry but no auth configured");
87 return Ok(false);
88 }
89
90 // Resolve all matching keys
91 let resolved_auths = match self
92 .resolver
93 .resolve_sig_key(&entry.sig.key, auth_settings, instance)
94 .await
95 {
96 Ok(auths) => auths,
97 Err(e) => {
98 debug!("Key resolution failed: {:?}", e);
99 return Ok(false);
100 }
101 };
102
103 // The claimed tips in a delegation SigKey pin resolution: the delegated
104 // tree's auth settings are read as of those tips (not its live head), and
105 // the tips may not regress below the snapshot the parent tree committed
106 // for the delegation (see DelegationResolver::resolve_delegation_path_with_depth).
107 //
108 // FIXME(security): this is the settings-pointer floor only — still a known
109 // gap. Two hardening steps remain: (1) strict per-entry non-regression
110 // (sibling entries above the committed floor can still pick different
111 // snapshots), and (2) a monotonic gate on settings writes so the committed
112 // pointer itself cannot be moved backwards. Until both land, the floor
113 // bounds regression but does not fully pin the delegated-tree snapshot.
114
115 // Determine operation type from entry content
116 let operation = if entry.subtrees().contains(&SETTINGS.to_string()) {
117 Operation::WriteSettings
118 } else {
119 Operation::WriteData
120 };
121
122 // Try signature verification + permission check against each candidate
123 for resolved_auth in resolved_auths {
124 // Skip keys that do not currently grant access
125 if !resolved_auth.grants_access() {
126 debug!("Skipping inactive key: {:?}", resolved_auth.key_status);
127 continue;
128 }
129
130 // Try to verify the signature with this key
131 if verify_entry_signature(entry, &resolved_auth.public_key).is_ok() {
132 debug!("Signature verified, checking permissions");
133 // Signature verified - now check permissions
134 if self.check_permissions(&resolved_auth, &operation)? {
135 debug!("Entry valid: signature verified with sufficient permissions");
136 return Ok(true);
137 }
138 debug!("Signature valid but insufficient permissions, trying next key");
139 // Continue to try other keys that might have higher permissions
140 }
141 }
142
143 // No key verified with sufficient permissions
144 debug!("Entry invalid: no key verified with sufficient permissions");
145 Ok(false)
146 }
147
148 /// Resolve authentication identifier to concrete authentication information
149 ///
150 /// Returns all matching ResolvedAuth entries. For name hints that match
151 /// multiple keys, all matches are returned so the caller can try signature
152 /// verification against each.
153 ///
154 /// # Arguments
155 /// * `sig_key` - The signature key identifier to resolve
156 /// * `auth_settings` - Authentication settings containing auth configuration
157 /// * `instance` - Instance for loading delegated trees (required for Delegation sig_key)
158 pub async fn resolve_sig_key(
159 &mut self,
160 sig_key: &SigKey,
161 auth_settings: &AuthSettings,
162 instance: Option<&Instance>,
163 ) -> Result<Vec<ResolvedAuth>> {
164 // Delegate to the resolver
165 self.resolver
166 .resolve_sig_key(sig_key, auth_settings, instance)
167 .await
168 }
169
170 /// Check if a resolved authentication has sufficient permissions for an operation
171 pub fn check_permissions(
172 &self,
173 resolved: &ResolvedAuth,
174 operation: &Operation,
175 ) -> Result<bool> {
176 super::permissions::check_permissions(resolved, operation)
177 }
178
179 /// Clear the authentication cache
180 pub fn clear_cache(&mut self) {
181 self.auth_cache.clear();
182 self.resolver.clear_cache();
183 }
184}
185
186impl Default for AuthValidator {
187 fn default() -> Self {
188 Self::new()
189 }
190}