Skip to main content

routing/
policy.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::capability_source::{
6    AnonymizedAggregateSource, BuiltinSource, CapabilitySource, CapabilityToCapabilitySource,
7    ComponentSource, EnvironmentSource, FilteredAggregateProviderSource, FilteredProviderSource,
8    FrameworkSource, NamespaceSource, StorageBackingDirectorySource, VoidSource,
9};
10use cm_config::{
11    AllowlistEntry, AllowlistMatcher, CapabilityAllowlistKey, CapabilityAllowlistSource,
12    DebugCapabilityKey, SecurityPolicy,
13};
14use log::{error, warn};
15use moniker::{ExtendedMoniker, Moniker};
16use std::sync::Arc;
17use thiserror::Error;
18use zx_status as zx;
19
20use cm_rust::CapabilityTypeName;
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Serialize};
23
24/// Errors returned by the PolicyChecker and the ScopedPolicyChecker.
25#[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(rename_all = "snake_case"))]
26#[derive(Debug, Clone, Error, PartialEq)]
27pub enum PolicyError {
28    #[error("security policy disallows \"{policy}\" job policy for \"{moniker}\"")]
29    JobPolicyDisallowed { policy: String, moniker: Moniker },
30
31    #[error("security policy disallows \"{policy}\" child policy for \"{moniker}\"")]
32    ChildPolicyDisallowed { policy: String, moniker: Moniker },
33
34    #[error(
35        "security policy was unable to extract the source from the routed capability at component \"{moniker}\""
36    )]
37    InvalidCapabilitySource { moniker: ExtendedMoniker },
38
39    #[error(
40        "security policy disallows \"{cap}\" from \"{source_moniker}\" being used at \"{target_moniker}\""
41    )]
42    CapabilityUseDisallowed {
43        cap: String,
44        source_moniker: ExtendedMoniker,
45        target_moniker: Moniker,
46    },
47
48    #[error(
49        "debug security policy disallows \"{cap}\" from being registered in \
50        environment \"{env_name}\" at \"{env_moniker}\""
51    )]
52    DebugCapabilityUseDisallowed { cap: String, env_moniker: Moniker, env_name: String },
53}
54
55impl PolicyError {
56    /// Convert this error into its approximate `zx::Status` equivalent.
57    pub fn as_zx_status(&self) -> zx::Status {
58        zx::Status::ACCESS_DENIED
59    }
60}
61
62impl From<PolicyError> for ExtendedMoniker {
63    fn from(err: PolicyError) -> ExtendedMoniker {
64        match err {
65            PolicyError::ChildPolicyDisallowed { moniker, .. }
66            | PolicyError::DebugCapabilityUseDisallowed { env_moniker: moniker, .. }
67            | PolicyError::JobPolicyDisallowed { moniker, .. } => moniker.into(),
68
69            PolicyError::CapabilityUseDisallowed { source_moniker: moniker, .. }
70            | PolicyError::InvalidCapabilitySource { moniker } => moniker,
71        }
72    }
73}
74
75/// Evaluates security policy globally across the entire Model and all components.
76/// This is used to enforce runtime capability routing restrictions across all
77/// components to prevent high privilleged capabilities from being routed to
78/// components outside of the list defined in the runtime security policy.
79#[derive(Clone, Debug, Default)]
80pub struct GlobalPolicyChecker {
81    /// The security policy to apply.
82    policy: Arc<SecurityPolicy>,
83}
84
85impl GlobalPolicyChecker {
86    /// Constructs a new PolicyChecker object configured by the SecurityPolicy.
87    pub fn new(policy: Arc<SecurityPolicy>) -> Self {
88        Self { policy }
89    }
90
91    fn get_policy_key(
92        capability_source: &CapabilitySource,
93    ) -> Result<CapabilityAllowlistKey, PolicyError> {
94        Ok(match &capability_source {
95            CapabilitySource::Namespace(NamespaceSource { capability, .. }) => {
96                CapabilityAllowlistKey {
97                    source_moniker: ExtendedMoniker::ComponentManager,
98                    source_name: capability
99                        .source_name()
100                        .ok_or(PolicyError::InvalidCapabilitySource {
101                            moniker: capability_source.source_moniker(),
102                        })?
103                        .clone(),
104                    source: CapabilityAllowlistSource::Self_,
105                    capability: capability.type_name(),
106                }
107            }
108            CapabilitySource::Component(ComponentSource { capability, moniker })
109            | CapabilitySource::StorageBackingDirectory(StorageBackingDirectorySource {
110                capability,
111                moniker,
112                ..
113            }) => CapabilityAllowlistKey {
114                source_moniker: ExtendedMoniker::ComponentInstance(moniker.clone()),
115                source_name: capability
116                    .source_name()
117                    .ok_or(PolicyError::InvalidCapabilitySource {
118                        moniker: capability_source.source_moniker(),
119                    })?
120                    .clone(),
121                source: CapabilityAllowlistSource::Self_,
122                capability: capability.type_name(),
123            },
124            CapabilitySource::Builtin(BuiltinSource { capability, .. }) => CapabilityAllowlistKey {
125                source_moniker: ExtendedMoniker::ComponentManager,
126                source_name: capability.source_name().clone(),
127                source: CapabilityAllowlistSource::Self_,
128                capability: capability.type_name(),
129            },
130            CapabilitySource::Framework(FrameworkSource { capability, moniker }) => {
131                CapabilityAllowlistKey {
132                    source_moniker: ExtendedMoniker::ComponentInstance(moniker.clone()),
133                    source_name: capability.source_name().clone(),
134                    source: CapabilityAllowlistSource::Framework,
135                    capability: capability.type_name(),
136                }
137            }
138            CapabilitySource::Void(VoidSource { capability, moniker }) => CapabilityAllowlistKey {
139                source_moniker: ExtendedMoniker::ComponentInstance(moniker.clone()),
140                source_name: capability.source_name().clone(),
141                source: CapabilityAllowlistSource::Void,
142                capability: capability.type_name(),
143            },
144            CapabilitySource::Capability(CapabilityToCapabilitySource {
145                source_capability,
146                moniker,
147            }) => CapabilityAllowlistKey {
148                source_moniker: ExtendedMoniker::ComponentInstance(moniker.clone()),
149                source_name: source_capability
150                    .source_name()
151                    .ok_or(PolicyError::InvalidCapabilitySource {
152                        moniker: capability_source.source_moniker(),
153                    })?
154                    .clone(),
155                source: CapabilityAllowlistSource::Capability,
156                capability: source_capability.type_name(),
157            },
158            CapabilitySource::AnonymizedAggregate(AnonymizedAggregateSource {
159                capability,
160                moniker,
161                ..
162            })
163            | CapabilitySource::FilteredProvider(FilteredProviderSource {
164                capability,
165                moniker,
166                ..
167            })
168            | CapabilitySource::FilteredAggregateProvider(FilteredAggregateProviderSource {
169                capability,
170                moniker,
171                ..
172            }) => CapabilityAllowlistKey {
173                source_moniker: ExtendedMoniker::ComponentInstance(moniker.clone()),
174                source_name: capability.source_name().clone(),
175                source: CapabilityAllowlistSource::Self_,
176                capability: capability.type_name(),
177            },
178            CapabilitySource::Environment(EnvironmentSource { capability, .. }) => {
179                CapabilityAllowlistKey {
180                    source_moniker: ExtendedMoniker::ComponentManager,
181                    source_name: capability
182                        .source_name()
183                        .ok_or(PolicyError::InvalidCapabilitySource {
184                            moniker: capability_source.source_moniker(),
185                        })?
186                        .clone(),
187                    source: CapabilityAllowlistSource::Environment,
188                    capability: capability.type_name(),
189                }
190            }
191            CapabilitySource::RemotedAt(moniker) => {
192                return Err(PolicyError::InvalidCapabilitySource {
193                    moniker: ExtendedMoniker::ComponentInstance(moniker.clone()),
194                });
195            }
196        })
197    }
198
199    /// Returns Ok(()) if the provided capability source can be routed to the
200    /// given target_moniker, else a descriptive PolicyError.
201    pub fn can_route_capability<'a>(
202        &self,
203        capability_source: &'a CapabilitySource,
204        target_moniker: &'a Moniker,
205    ) -> Result<(), PolicyError> {
206        let policy_key = Self::get_policy_key(capability_source).map_err(|e| {
207            error!("Security policy could not generate a policy key for `{}`", capability_source);
208            e
209        })?;
210
211        match self.policy.capability_policy.get(&policy_key) {
212            Some(entries) => {
213                let parts = target_moniker
214                    .path()
215                    .iter()
216                    .map(|c| AllowlistMatcher::Exact((*c).into()))
217                    .collect();
218                let entry = AllowlistEntry { matchers: parts };
219
220                // Use the HashSet to find any exact matches quickly.
221                if entries.contains(&entry) {
222                    return Ok(());
223                }
224
225                // Otherwise linear search for any non-exact matches.
226                if entries.iter().any(|entry| entry.matches(&target_moniker)) {
227                    Ok(())
228                } else {
229                    warn!(
230                        "Security policy prevented `{}` from `{}` being routed to `{}`.",
231                        policy_key.source_name, policy_key.source_moniker, target_moniker
232                    );
233                    Err(PolicyError::CapabilityUseDisallowed {
234                        cap: policy_key.source_name.to_string(),
235                        source_moniker: policy_key.source_moniker.to_owned(),
236                        target_moniker: target_moniker.to_owned(),
237                    })
238                }
239            }
240            None => Ok(()),
241        }
242    }
243
244    /// Returns Ok(()) if the provided debug capability source is allowed to be routed from given
245    /// environment.
246    pub fn can_register_debug_capability<'a>(
247        &self,
248        capability_type: CapabilityTypeName,
249        name: &'a cm_types::Name,
250        env_moniker: &'a Moniker,
251        env_name: &'a cm_types::Name,
252    ) -> Result<(), PolicyError> {
253        let debug_key = DebugCapabilityKey {
254            name: name.clone(),
255            source: CapabilityAllowlistSource::Self_,
256            capability: capability_type,
257            env_name: env_name.clone(),
258        };
259        let route_allowed = match self.policy.debug_capability_policy.get(&debug_key) {
260            None => false,
261            Some(allowlist_set) => allowlist_set.iter().any(|entry| entry.matches(env_moniker)),
262        };
263        if route_allowed {
264            return Ok(());
265        }
266
267        warn!(
268            "Debug security policy prevented `{}` from being registered to environment `{}` in `{}`.",
269            debug_key.name, env_name, env_moniker,
270        );
271        Err(PolicyError::DebugCapabilityUseDisallowed {
272            cap: debug_key.name.to_string(),
273            env_moniker: env_moniker.to_owned(),
274            env_name: env_name.to_string(),
275        })
276    }
277
278    /// Returns Ok(()) if `target_moniker` is allowed to have `on_terminate=REBOOT` set.
279    pub fn reboot_on_terminate_allowed(&self, target_moniker: &Moniker) -> Result<(), PolicyError> {
280        self.policy
281            .child_policy
282            .reboot_on_terminate
283            .iter()
284            .any(|entry| entry.matches(&target_moniker))
285            .then_some(())
286            .ok_or_else(|| PolicyError::ChildPolicyDisallowed {
287                policy: "reboot_on_terminate".to_owned(),
288                moniker: target_moniker.to_owned(),
289            })
290    }
291}
292
293/// Evaluates security policy relative to a specific Component (based on that Component's
294/// Moniker).
295#[derive(Clone)]
296pub struct ScopedPolicyChecker {
297    /// The security policy to apply.
298    policy: Arc<SecurityPolicy>,
299
300    /// The moniker of the component that policy will be evaluated for.
301    pub scope: Moniker,
302}
303
304impl ScopedPolicyChecker {
305    pub fn new(policy: Arc<SecurityPolicy>, scope: Moniker) -> Self {
306        ScopedPolicyChecker { policy, scope }
307    }
308
309    // This interface is super simple for now since there's only three allowlists. In the future
310    // we'll probably want a different interface than an individual function per policy item.
311
312    pub fn ambient_mark_vmo_exec_allowed(&self) -> Result<(), PolicyError> {
313        self.policy
314            .job_policy
315            .ambient_mark_vmo_exec
316            .iter()
317            .any(|entry| entry.matches(&self.scope))
318            .then_some(())
319            .ok_or_else(|| PolicyError::JobPolicyDisallowed {
320                policy: "ambient_mark_vmo_exec".to_owned(),
321                moniker: self.scope.to_owned(),
322            })
323    }
324
325    pub fn main_process_critical_allowed(&self) -> Result<(), PolicyError> {
326        self.policy
327            .job_policy
328            .main_process_critical
329            .iter()
330            .any(|entry| entry.matches(&self.scope))
331            .then_some(())
332            .ok_or_else(|| PolicyError::JobPolicyDisallowed {
333                policy: "main_process_critical".to_owned(),
334                moniker: self.scope.to_owned(),
335            })
336    }
337
338    pub fn create_raw_processes_allowed(&self) -> Result<(), PolicyError> {
339        self.policy
340            .job_policy
341            .create_raw_processes
342            .iter()
343            .any(|entry| entry.matches(&self.scope))
344            .then_some(())
345            .ok_or_else(|| PolicyError::JobPolicyDisallowed {
346                policy: "create_raw_processes".to_owned(),
347                moniker: self.scope.to_owned(),
348            })
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use assert_matches::assert_matches;
356    use cm_config::{AllowlistEntryBuilder, ChildPolicyAllowlists, JobPolicyAllowlists};
357    use moniker::ChildName;
358    use std::collections::HashMap;
359
360    #[test]
361    fn scoped_policy_checker_vmex() {
362        macro_rules! assert_vmex_allowed_matches {
363            ($policy:expr, $moniker:expr, $expected:pat) => {
364                let result = ScopedPolicyChecker::new($policy.clone(), $moniker.clone())
365                    .ambient_mark_vmo_exec_allowed();
366                assert_matches!(result, $expected);
367            };
368        }
369        macro_rules! assert_vmex_disallowed {
370            ($policy:expr, $moniker:expr) => {
371                assert_vmex_allowed_matches!(
372                    $policy,
373                    $moniker,
374                    Err(PolicyError::JobPolicyDisallowed { .. })
375                );
376            };
377        }
378        let policy = Arc::new(SecurityPolicy::default());
379        assert_vmex_disallowed!(policy, Moniker::root());
380        assert_vmex_disallowed!(policy, Moniker::try_from(["foo"]).unwrap());
381
382        let allowed1 = Moniker::try_from(["foo", "bar"]).unwrap();
383        let allowed2 = Moniker::try_from(["baz", "fiz"]).unwrap();
384        let policy = Arc::new(SecurityPolicy {
385            job_policy: JobPolicyAllowlists {
386                ambient_mark_vmo_exec: vec![
387                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
388                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
389                ],
390                main_process_critical: vec![
391                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
392                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
393                ],
394                create_raw_processes: vec![
395                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
396                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
397                ],
398            },
399            capability_policy: HashMap::new(),
400            debug_capability_policy: HashMap::new(),
401            child_policy: ChildPolicyAllowlists {
402                reboot_on_terminate: vec![
403                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
404                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
405                ],
406            },
407        });
408        assert_vmex_allowed_matches!(policy, allowed1, Ok(()));
409        assert_vmex_allowed_matches!(policy, allowed2, Ok(()));
410        assert_vmex_disallowed!(policy, Moniker::root());
411        assert_vmex_disallowed!(policy, allowed1.parent().unwrap());
412        assert_vmex_disallowed!(policy, allowed1.child(ChildName::try_from("baz").unwrap()));
413    }
414
415    #[test]
416    fn scoped_policy_checker_create_raw_processes() {
417        macro_rules! assert_create_raw_processes_allowed_matches {
418            ($policy:expr, $moniker:expr, $expected:pat) => {
419                let result = ScopedPolicyChecker::new($policy.clone(), $moniker.clone())
420                    .create_raw_processes_allowed();
421                assert_matches!(result, $expected);
422            };
423        }
424        macro_rules! assert_create_raw_processes_disallowed {
425            ($policy:expr, $moniker:expr) => {
426                assert_create_raw_processes_allowed_matches!(
427                    $policy,
428                    $moniker,
429                    Err(PolicyError::JobPolicyDisallowed { .. })
430                );
431            };
432        }
433        let policy = Arc::new(SecurityPolicy::default());
434        assert_create_raw_processes_disallowed!(policy, Moniker::root());
435        assert_create_raw_processes_disallowed!(policy, Moniker::try_from(["foo"]).unwrap());
436
437        let allowed1 = Moniker::try_from(["foo", "bar"]).unwrap();
438        let allowed2 = Moniker::try_from(["baz", "fiz"]).unwrap();
439        let policy = Arc::new(SecurityPolicy {
440            job_policy: JobPolicyAllowlists {
441                ambient_mark_vmo_exec: vec![],
442                main_process_critical: vec![],
443                create_raw_processes: vec![
444                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
445                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
446                ],
447            },
448            capability_policy: HashMap::new(),
449            debug_capability_policy: HashMap::new(),
450            child_policy: ChildPolicyAllowlists { reboot_on_terminate: vec![] },
451        });
452        assert_create_raw_processes_allowed_matches!(policy, allowed1, Ok(()));
453        assert_create_raw_processes_allowed_matches!(policy, allowed2, Ok(()));
454        assert_create_raw_processes_disallowed!(policy, Moniker::root());
455        assert_create_raw_processes_disallowed!(policy, allowed1.parent().unwrap());
456        assert_create_raw_processes_disallowed!(
457            policy,
458            allowed1.child(ChildName::try_from("baz").unwrap())
459        );
460    }
461
462    #[test]
463    fn scoped_policy_checker_main_process_critical_allowed() {
464        macro_rules! assert_critical_allowed_matches {
465            ($policy:expr, $moniker:expr, $expected:pat) => {
466                let result = ScopedPolicyChecker::new($policy.clone(), $moniker.clone())
467                    .main_process_critical_allowed();
468                assert_matches!(result, $expected);
469            };
470        }
471        macro_rules! assert_critical_disallowed {
472            ($policy:expr, $moniker:expr) => {
473                assert_critical_allowed_matches!(
474                    $policy,
475                    $moniker,
476                    Err(PolicyError::JobPolicyDisallowed { .. })
477                );
478            };
479        }
480        let policy = Arc::new(SecurityPolicy::default());
481        assert_critical_disallowed!(policy, Moniker::root());
482        assert_critical_disallowed!(policy, Moniker::try_from(["foo"]).unwrap());
483
484        let allowed1 = Moniker::try_from(["foo", "bar"]).unwrap();
485        let allowed2 = Moniker::try_from(["baz", "fiz"]).unwrap();
486        let policy = Arc::new(SecurityPolicy {
487            job_policy: JobPolicyAllowlists {
488                ambient_mark_vmo_exec: vec![
489                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
490                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
491                ],
492                main_process_critical: vec![
493                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
494                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
495                ],
496                create_raw_processes: vec![
497                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
498                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
499                ],
500            },
501            capability_policy: HashMap::new(),
502            debug_capability_policy: HashMap::new(),
503            child_policy: ChildPolicyAllowlists { reboot_on_terminate: vec![] },
504        });
505        assert_critical_allowed_matches!(policy, allowed1, Ok(()));
506        assert_critical_allowed_matches!(policy, allowed2, Ok(()));
507        assert_critical_disallowed!(policy, Moniker::root());
508        assert_critical_disallowed!(policy, allowed1.parent().unwrap());
509        assert_critical_disallowed!(policy, allowed1.child(ChildName::try_from("baz").unwrap()));
510    }
511
512    #[test]
513    fn scoped_policy_checker_reboot_policy_allowed() {
514        macro_rules! assert_reboot_allowed_matches {
515            ($policy:expr, $moniker:expr, $expected:pat) => {
516                let result = GlobalPolicyChecker::new($policy.clone())
517                    .reboot_on_terminate_allowed(&$moniker);
518                assert_matches!(result, $expected);
519            };
520        }
521        macro_rules! assert_reboot_disallowed {
522            ($policy:expr, $moniker:expr) => {
523                assert_reboot_allowed_matches!(
524                    $policy,
525                    $moniker,
526                    Err(PolicyError::ChildPolicyDisallowed { .. })
527                );
528            };
529        }
530
531        // Empty policy and enabled.
532        let policy = Arc::new(SecurityPolicy::default());
533        assert_reboot_disallowed!(policy, Moniker::root());
534        assert_reboot_disallowed!(policy, Moniker::try_from(["foo"]).unwrap());
535
536        // Nonempty policy.
537        let allowed1 = Moniker::try_from(["foo", "bar"]).unwrap();
538        let allowed2 = Moniker::try_from(["baz", "fiz"]).unwrap();
539        let policy = Arc::new(SecurityPolicy {
540            job_policy: JobPolicyAllowlists {
541                ambient_mark_vmo_exec: vec![],
542                main_process_critical: vec![],
543                create_raw_processes: vec![],
544            },
545            capability_policy: HashMap::new(),
546            debug_capability_policy: HashMap::new(),
547            child_policy: ChildPolicyAllowlists {
548                reboot_on_terminate: vec![
549                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
550                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
551                ],
552            },
553        });
554        assert_reboot_allowed_matches!(policy, allowed1, Ok(()));
555        assert_reboot_allowed_matches!(policy, allowed2, Ok(()));
556        assert_reboot_disallowed!(policy, Moniker::root());
557        assert_reboot_disallowed!(policy, allowed1.parent().unwrap());
558        assert_reboot_disallowed!(policy, allowed1.child(ChildName::try_from("baz").unwrap()));
559    }
560}