Skip to main content

caxton/domain/
security.rs

1use crate::domain_types::{MaxExports, MaxImportFunctions};
2use nutype::nutype;
3#[allow(unused_imports)]
4use serde::{Deserialize, Serialize};
5use std::collections::HashSet;
6
7#[nutype(
8    validate(len_char_min = 1, len_char_max = 255),
9    derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display)
10)]
11pub struct SafeFunctionName(String);
12
13impl SafeFunctionName {
14    pub fn is_messaging_function(&self) -> bool {
15        self.to_string().starts_with("agent_message_")
16    }
17
18    pub fn is_standard_function(&self) -> bool {
19        const STANDARD_FUNCTIONS: &[&str] = &[
20            "agent_get_id",
21            "agent_get_timestamp",
22            "agent_log",
23            "agent_message_send",
24            "agent_message_receive",
25        ];
26        STANDARD_FUNCTIONS.contains(&self.to_string().as_str())
27    }
28}
29
30#[nutype(
31    validate(len_char_min = 1, len_char_max = 255),
32    derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display)
33)]
34pub struct UnsafeFunctionName(String);
35
36impl UnsafeFunctionName {
37    pub fn is_memory_function(&self) -> bool {
38        self.to_string().starts_with("memory_")
39    }
40
41    pub fn is_system_function(&self) -> bool {
42        self.to_string().starts_with("system_") || self.to_string().starts_with("process_")
43    }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub enum FunctionName {
48    Safe(SafeFunctionName),
49    Unsafe(UnsafeFunctionName),
50}
51
52impl FunctionName {
53    /// Create a function name from string, categorizing as safe or unsafe
54    ///
55    /// # Panics
56    ///
57    /// Panics if the function name cannot be created (should never happen for valid strings).
58    pub fn categorize_function(name: &str) -> Self {
59        const UNSAFE_FUNCTIONS: &[&str] = &[
60            "memory_grow",
61            "memory_copy",
62            "table_grow",
63            "table_copy",
64            "process_exit",
65            "system_call",
66            "fd_write",
67            "fd_read",
68            "environ_get",
69            "environ_sizes_get",
70        ];
71
72        if UNSAFE_FUNCTIONS.contains(&name) {
73            FunctionName::Unsafe(UnsafeFunctionName::try_new(name.to_string()).unwrap())
74        } else {
75            SafeFunctionName::try_new(name.to_string()).map_or_else(
76                |_| FunctionName::Unsafe(UnsafeFunctionName::try_new(name.to_string()).unwrap()),
77                FunctionName::Safe,
78            )
79        }
80    }
81
82    pub fn is_safe(&self) -> bool {
83        matches!(self, FunctionName::Safe(_))
84    }
85
86    pub fn as_str(&self) -> String {
87        match self {
88            FunctionName::Safe(name) => name.to_string(),
89            FunctionName::Unsafe(name) => name.to_string(),
90        }
91    }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct StrictSecurityPolicy {
96    pub max_import_functions: MaxImportFunctions,
97    pub max_exports: MaxExports,
98    pub allowed_functions: Vec<SafeFunctionName>,
99}
100
101impl StrictSecurityPolicy {
102    /// Create a new strict security policy
103    ///
104    /// # Panics
105    ///
106    /// Panics if the hardcoded function names are invalid (should never happen).
107    pub fn new(max_import_functions: MaxImportFunctions, max_exports: MaxExports) -> Self {
108        let allowed_functions = vec![
109            SafeFunctionName::try_new("agent_get_id".to_string()).unwrap(),
110            SafeFunctionName::try_new("agent_get_timestamp".to_string()).unwrap(),
111            SafeFunctionName::try_new("agent_log".to_string()).unwrap(),
112        ];
113
114        Self {
115            max_import_functions,
116            max_exports,
117            allowed_functions,
118        }
119    }
120
121    pub fn enable_networking(&self) -> bool {
122        false
123    }
124
125    pub fn enable_threads(&self) -> bool {
126        false
127    }
128
129    pub fn enable_fuel_metering(&self) -> bool {
130        true
131    }
132
133    pub fn is_function_allowed(&self, function: &FunctionName) -> bool {
134        match function {
135            FunctionName::Safe(name) => self.allowed_functions.contains(name),
136            FunctionName::Unsafe(_) => false,
137        }
138    }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct RelaxedSecurityPolicy {
143    pub max_import_functions: MaxImportFunctions,
144    pub max_exports: MaxExports,
145    pub allowed_functions: HashSet<FunctionName>,
146    pub enable_threads: bool,
147    pub enable_networking: bool,
148}
149
150impl RelaxedSecurityPolicy {
151    /// Create a new relaxed security policy
152    ///
153    /// # Panics
154    ///
155    /// Panics if the hardcoded function names are invalid (should never happen).
156    pub fn new(
157        max_import_functions: MaxImportFunctions,
158        max_exports: MaxExports,
159        enable_threads: bool,
160        enable_networking: bool,
161    ) -> Self {
162        let mut allowed_functions = HashSet::new();
163
164        allowed_functions.insert(FunctionName::Safe(
165            SafeFunctionName::try_new("agent_get_id".to_string()).unwrap(),
166        ));
167        allowed_functions.insert(FunctionName::Safe(
168            SafeFunctionName::try_new("agent_get_timestamp".to_string()).unwrap(),
169        ));
170        allowed_functions.insert(FunctionName::Safe(
171            SafeFunctionName::try_new("agent_log".to_string()).unwrap(),
172        ));
173        allowed_functions.insert(FunctionName::Safe(
174            SafeFunctionName::try_new("agent_message_send".to_string()).unwrap(),
175        ));
176        allowed_functions.insert(FunctionName::Safe(
177            SafeFunctionName::try_new("agent_message_receive".to_string()).unwrap(),
178        ));
179
180        if enable_networking {
181            allowed_functions.insert(FunctionName::Safe(
182                SafeFunctionName::try_new("network_connect".to_string()).unwrap(),
183            ));
184            allowed_functions.insert(FunctionName::Safe(
185                SafeFunctionName::try_new("network_send".to_string()).unwrap(),
186            ));
187            allowed_functions.insert(FunctionName::Safe(
188                SafeFunctionName::try_new("network_receive".to_string()).unwrap(),
189            ));
190        }
191
192        Self {
193            max_import_functions,
194            max_exports,
195            allowed_functions,
196            enable_threads,
197            enable_networking,
198        }
199    }
200
201    pub fn enable_fuel_metering(&self) -> bool {
202        !self.enable_threads
203    }
204
205    pub fn is_function_allowed(&self, function: &FunctionName) -> bool {
206        self.allowed_functions.contains(function)
207    }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub enum SecurityLevel {
212    Strict(StrictSecurityPolicy),
213    Relaxed(RelaxedSecurityPolicy),
214}
215
216impl SecurityLevel {
217    pub fn validate(&self) -> bool {
218        match self {
219            SecurityLevel::Strict(policy) => {
220                policy.max_import_functions.into_inner() > 0 && policy.max_exports.into_inner() > 0
221            }
222            SecurityLevel::Relaxed(policy) => {
223                (policy.enable_fuel_metering() || !policy.enable_threads)
224                    && policy.max_exports.into_inner() > 0
225                    && policy.max_import_functions.into_inner() > 0
226            }
227        }
228    }
229
230    pub fn is_function_allowed(&self, function: &FunctionName) -> bool {
231        match self {
232            SecurityLevel::Strict(policy) => policy.is_function_allowed(function),
233            SecurityLevel::Relaxed(policy) => policy.is_function_allowed(function),
234        }
235    }
236}
237
238pub struct ValidatedSecurityPolicy {
239    level: SecurityLevel,
240}
241
242impl ValidatedSecurityPolicy {
243    pub fn new(level: SecurityLevel) -> Option<Self> {
244        if level.validate() {
245            Some(Self { level })
246        } else {
247            None
248        }
249    }
250
251    pub fn level(&self) -> &SecurityLevel {
252        &self.level
253    }
254}