| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| crate::{capability_source::CapabilitySource, component_instance::ComponentInstanceInterface}, |
| cm_config::{ |
| AllowlistEntry, AllowlistMatcher, CapabilityAllowlistKey, CapabilityAllowlistSource, |
| DebugCapabilityKey, SecurityPolicy, |
| }, |
| fuchsia_zircon_status as zx, |
| moniker::{ExtendedMoniker, Moniker, MonikerBase}, |
| std::sync::Arc, |
| thiserror::Error, |
| tracing::{error, warn}, |
| }; |
| |
| use cm_rust::CapabilityTypeName; |
| #[cfg(feature = "serde")] |
| use serde::{Deserialize, Serialize}; |
| |
| /// Errors returned by the PolicyChecker and the ScopedPolicyChecker. |
| #[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(rename_all = "snake_case"))] |
| #[derive(Debug, Clone, Error, PartialEq)] |
| pub enum PolicyError { |
| #[error("security policy disallows \"{policy}\" job policy for \"{moniker}\"")] |
| JobPolicyDisallowed { policy: String, moniker: Moniker }, |
| |
| #[error("security policy disallows \"{policy}\" child policy for \"{moniker}\"")] |
| ChildPolicyDisallowed { policy: String, moniker: Moniker }, |
| |
| #[error("security policy was unable to extract the source from the routed capability")] |
| InvalidCapabilitySource, |
| |
| #[error("security policy disallows \"{cap}\" from \"{source_moniker}\" being used at \"{target_moniker}\"")] |
| CapabilityUseDisallowed { |
| cap: String, |
| source_moniker: ExtendedMoniker, |
| target_moniker: Moniker, |
| }, |
| |
| #[error( |
| "debug security policy disallows \"{cap}\" from being registered in \ |
| environment \"{env_name}\" at \"{env_moniker}\"" |
| )] |
| DebugCapabilityUseDisallowed { cap: String, env_moniker: Moniker, env_name: String }, |
| } |
| |
| impl PolicyError { |
| /// Convert this error into its approximate `zx::Status` equivalent. |
| pub fn as_zx_status(&self) -> zx::Status { |
| zx::Status::ACCESS_DENIED |
| } |
| } |
| |
| /// Evaluates security policy globally across the entire Model and all components. |
| /// This is used to enforce runtime capability routing restrictions across all |
| /// components to prevent high privilleged capabilities from being routed to |
| /// components outside of the list defined in the runtime security policy. |
| #[derive(Clone, Debug, Default)] |
| pub struct GlobalPolicyChecker { |
| /// The security policy to apply. |
| policy: Arc<SecurityPolicy>, |
| } |
| |
| impl GlobalPolicyChecker { |
| /// Constructs a new PolicyChecker object configured by the SecurityPolicy. |
| pub fn new(policy: Arc<SecurityPolicy>) -> Self { |
| Self { policy } |
| } |
| |
| fn get_policy_key<'a, C>( |
| capability_source: &'a CapabilitySource<C>, |
| ) -> Result<CapabilityAllowlistKey, PolicyError> |
| where |
| C: ComponentInstanceInterface, |
| { |
| Ok(match &capability_source { |
| CapabilitySource::Namespace { capability, .. } => CapabilityAllowlistKey { |
| source_moniker: ExtendedMoniker::ComponentManager, |
| source_name: capability |
| .source_name() |
| .ok_or(PolicyError::InvalidCapabilitySource)? |
| .clone(), |
| source: CapabilityAllowlistSource::Self_, |
| capability: capability.type_name(), |
| }, |
| CapabilitySource::Component { capability, component } => CapabilityAllowlistKey { |
| source_moniker: ExtendedMoniker::ComponentInstance(component.moniker.clone()), |
| source_name: capability |
| .source_name() |
| .ok_or(PolicyError::InvalidCapabilitySource)? |
| .clone(), |
| source: CapabilityAllowlistSource::Self_, |
| capability: capability.type_name(), |
| }, |
| CapabilitySource::Builtin { capability, .. } => CapabilityAllowlistKey { |
| source_moniker: ExtendedMoniker::ComponentManager, |
| source_name: capability.source_name().clone(), |
| source: CapabilityAllowlistSource::Self_, |
| capability: capability.type_name(), |
| }, |
| CapabilitySource::Framework { capability, component } => CapabilityAllowlistKey { |
| source_moniker: ExtendedMoniker::ComponentInstance(component.moniker.clone()), |
| source_name: capability.source_name().clone(), |
| source: CapabilityAllowlistSource::Framework, |
| capability: capability.type_name(), |
| }, |
| CapabilitySource::Void { capability, component } => CapabilityAllowlistKey { |
| source_moniker: ExtendedMoniker::ComponentInstance(component.moniker.clone()), |
| source_name: capability.source_name().clone(), |
| source: CapabilityAllowlistSource::Void, |
| capability: capability.type_name(), |
| }, |
| CapabilitySource::Capability { source_capability, component } => { |
| CapabilityAllowlistKey { |
| source_moniker: ExtendedMoniker::ComponentInstance(component.moniker.clone()), |
| source_name: source_capability |
| .source_name() |
| .ok_or(PolicyError::InvalidCapabilitySource)? |
| .clone(), |
| source: CapabilityAllowlistSource::Capability, |
| capability: source_capability.type_name(), |
| } |
| } |
| CapabilitySource::AnonymizedAggregate { capability, component, .. } |
| | CapabilitySource::FilteredAggregate { capability, component, .. } => { |
| CapabilityAllowlistKey { |
| source_moniker: ExtendedMoniker::ComponentInstance(component.moniker.clone()), |
| source_name: capability.source_name().clone(), |
| source: CapabilityAllowlistSource::Self_, |
| capability: capability.type_name(), |
| } |
| } |
| CapabilitySource::Environment { capability, .. } => CapabilityAllowlistKey { |
| source_moniker: ExtendedMoniker::ComponentManager, |
| source_name: capability |
| .source_name() |
| .ok_or(PolicyError::InvalidCapabilitySource)? |
| .clone(), |
| source: CapabilityAllowlistSource::Environment, |
| capability: capability.type_name(), |
| }, |
| }) |
| } |
| |
| /// Returns Ok(()) if the provided capability source can be routed to the |
| /// given target_moniker, else a descriptive PolicyError. |
| pub fn can_route_capability<'a, C>( |
| &self, |
| capability_source: &'a CapabilitySource<C>, |
| target_moniker: &'a Moniker, |
| ) -> Result<(), PolicyError> |
| where |
| C: ComponentInstanceInterface, |
| { |
| let policy_key = Self::get_policy_key(capability_source).map_err(|e| { |
| error!("Security policy could not generate a policy key for `{}`", capability_source); |
| e |
| })?; |
| |
| match self.policy.capability_policy.get(&policy_key) { |
| Some(entries) => { |
| let parts = target_moniker |
| .path() |
| .clone() |
| .into_iter() |
| .map(|c| AllowlistMatcher::Exact(c)) |
| .collect(); |
| let entry = AllowlistEntry { matchers: parts }; |
| |
| // Use the HashSet to find any exact matches quickly. |
| if entries.contains(&entry) { |
| return Ok(()); |
| } |
| |
| // Otherwise linear search for any non-exact matches. |
| if entries.iter().any(|entry| entry.matches(&target_moniker)) { |
| Ok(()) |
| } else { |
| warn!( |
| "Security policy prevented `{}` from `{}` being routed to `{}`.", |
| policy_key.source_name, policy_key.source_moniker, target_moniker |
| ); |
| Err(PolicyError::CapabilityUseDisallowed { |
| cap: policy_key.source_name.to_string(), |
| source_moniker: policy_key.source_moniker.to_owned(), |
| target_moniker: target_moniker.to_owned(), |
| }) |
| } |
| } |
| None => Ok(()), |
| } |
| } |
| |
| /// Returns Ok(()) if the provided debug capability source is allowed to be routed from given |
| /// environment. |
| pub fn can_register_debug_capability<'a>( |
| &self, |
| capability_type: CapabilityTypeName, |
| name: &'a cm_types::Name, |
| env_moniker: &'a Moniker, |
| env_name: &'a cm_types::Name, |
| ) -> Result<(), PolicyError> { |
| let debug_key = DebugCapabilityKey { |
| name: name.clone(), |
| source: CapabilityAllowlistSource::Self_, |
| capability: capability_type, |
| env_name: env_name.clone(), |
| }; |
| let route_allowed = match self.policy.debug_capability_policy.get(&debug_key) { |
| None => false, |
| Some(allowlist_set) => allowlist_set.iter().any(|entry| entry.matches(env_moniker)), |
| }; |
| if route_allowed { |
| return Ok(()); |
| } |
| |
| warn!( |
| "Debug security policy prevented `{}` from being registered to environment `{}` in `{}`.", |
| debug_key.name, env_name, env_moniker, |
| ); |
| Err(PolicyError::DebugCapabilityUseDisallowed { |
| cap: debug_key.name.to_string(), |
| env_moniker: env_moniker.to_owned(), |
| env_name: env_name.to_string(), |
| }) |
| } |
| |
| /// Returns Ok(()) if `target_moniker` is allowed to have `on_terminate=REBOOT` set. |
| pub fn reboot_on_terminate_allowed(&self, target_moniker: &Moniker) -> Result<(), PolicyError> { |
| self.policy |
| .child_policy |
| .reboot_on_terminate |
| .iter() |
| .any(|entry| entry.matches(&target_moniker)) |
| .then(|| ()) |
| .ok_or_else(|| PolicyError::ChildPolicyDisallowed { |
| policy: "reboot_on_terminate".to_owned(), |
| moniker: target_moniker.to_owned(), |
| }) |
| } |
| } |
| |
| /// Evaluates security policy relative to a specific Component (based on that Component's |
| /// Moniker). |
| #[derive(Clone)] |
| pub struct ScopedPolicyChecker { |
| /// The security policy to apply. |
| policy: Arc<SecurityPolicy>, |
| |
| /// The moniker of the component that policy will be evaluated for. |
| pub scope: Moniker, |
| } |
| |
| impl ScopedPolicyChecker { |
| pub fn new(policy: Arc<SecurityPolicy>, scope: Moniker) -> Self { |
| ScopedPolicyChecker { policy, scope } |
| } |
| |
| // This interface is super simple for now since there's only three allowlists. In the future |
| // we'll probably want a different interface than an individual function per policy item. |
| |
| pub fn ambient_mark_vmo_exec_allowed(&self) -> Result<(), PolicyError> { |
| self.policy |
| .job_policy |
| .ambient_mark_vmo_exec |
| .iter() |
| .any(|entry| entry.matches(&self.scope)) |
| .then(|| ()) |
| .ok_or_else(|| PolicyError::JobPolicyDisallowed { |
| policy: "ambient_mark_vmo_exec".to_owned(), |
| moniker: self.scope.to_owned(), |
| }) |
| } |
| |
| pub fn main_process_critical_allowed(&self) -> Result<(), PolicyError> { |
| self.policy |
| .job_policy |
| .main_process_critical |
| .iter() |
| .any(|entry| entry.matches(&self.scope)) |
| .then(|| ()) |
| .ok_or_else(|| PolicyError::JobPolicyDisallowed { |
| policy: "main_process_critical".to_owned(), |
| moniker: self.scope.to_owned(), |
| }) |
| } |
| |
| pub fn create_raw_processes_allowed(&self) -> Result<(), PolicyError> { |
| self.policy |
| .job_policy |
| .create_raw_processes |
| .iter() |
| .any(|entry| entry.matches(&self.scope)) |
| .then(|| ()) |
| .ok_or_else(|| PolicyError::JobPolicyDisallowed { |
| policy: "create_raw_processes".to_owned(), |
| moniker: self.scope.to_owned(), |
| }) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| assert_matches::assert_matches, |
| cm_config::{AllowlistEntryBuilder, ChildPolicyAllowlists, JobPolicyAllowlists}, |
| moniker::ChildName, |
| std::collections::HashMap, |
| }; |
| |
| #[test] |
| fn scoped_policy_checker_vmex() { |
| macro_rules! assert_vmex_allowed_matches { |
| ($policy:expr, $moniker:expr, $expected:pat) => { |
| let result = ScopedPolicyChecker::new($policy.clone(), $moniker.clone()) |
| .ambient_mark_vmo_exec_allowed(); |
| assert_matches!(result, $expected); |
| }; |
| } |
| macro_rules! assert_vmex_disallowed { |
| ($policy:expr, $moniker:expr) => { |
| assert_vmex_allowed_matches!( |
| $policy, |
| $moniker, |
| Err(PolicyError::JobPolicyDisallowed { .. }) |
| ); |
| }; |
| } |
| let policy = Arc::new(SecurityPolicy::default()); |
| assert_vmex_disallowed!(policy, Moniker::root()); |
| assert_vmex_disallowed!(policy, Moniker::try_from(vec!["foo"]).unwrap()); |
| |
| let allowed1 = Moniker::try_from(vec!["foo", "bar"]).unwrap(); |
| let allowed2 = Moniker::try_from(vec!["baz", "fiz"]).unwrap(); |
| let policy = Arc::new(SecurityPolicy { |
| job_policy: JobPolicyAllowlists { |
| ambient_mark_vmo_exec: vec![ |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed1), |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed2), |
| ], |
| main_process_critical: vec![ |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed1), |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed2), |
| ], |
| create_raw_processes: vec![ |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed1), |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed2), |
| ], |
| }, |
| capability_policy: HashMap::new(), |
| debug_capability_policy: HashMap::new(), |
| child_policy: ChildPolicyAllowlists { |
| reboot_on_terminate: vec![ |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed1), |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed2), |
| ], |
| }, |
| }); |
| assert_vmex_allowed_matches!(policy, allowed1, Ok(())); |
| assert_vmex_allowed_matches!(policy, allowed2, Ok(())); |
| assert_vmex_disallowed!(policy, Moniker::root()); |
| assert_vmex_disallowed!(policy, allowed1.parent().unwrap()); |
| assert_vmex_disallowed!(policy, allowed1.child(ChildName::try_from("baz").unwrap())); |
| } |
| |
| #[test] |
| fn scoped_policy_checker_create_raw_processes() { |
| macro_rules! assert_create_raw_processes_allowed_matches { |
| ($policy:expr, $moniker:expr, $expected:pat) => { |
| let result = ScopedPolicyChecker::new($policy.clone(), $moniker.clone()) |
| .create_raw_processes_allowed(); |
| assert_matches!(result, $expected); |
| }; |
| } |
| macro_rules! assert_create_raw_processes_disallowed { |
| ($policy:expr, $moniker:expr) => { |
| assert_create_raw_processes_allowed_matches!( |
| $policy, |
| $moniker, |
| Err(PolicyError::JobPolicyDisallowed { .. }) |
| ); |
| }; |
| } |
| let policy = Arc::new(SecurityPolicy::default()); |
| assert_create_raw_processes_disallowed!(policy, Moniker::root()); |
| assert_create_raw_processes_disallowed!(policy, Moniker::try_from(vec!["foo"]).unwrap()); |
| |
| let allowed1 = Moniker::try_from(vec!["foo", "bar"]).unwrap(); |
| let allowed2 = Moniker::try_from(vec!["baz", "fiz"]).unwrap(); |
| let policy = Arc::new(SecurityPolicy { |
| job_policy: JobPolicyAllowlists { |
| ambient_mark_vmo_exec: vec![], |
| main_process_critical: vec![], |
| create_raw_processes: vec![ |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed1), |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed2), |
| ], |
| }, |
| capability_policy: HashMap::new(), |
| debug_capability_policy: HashMap::new(), |
| child_policy: ChildPolicyAllowlists { reboot_on_terminate: vec![] }, |
| }); |
| assert_create_raw_processes_allowed_matches!(policy, allowed1, Ok(())); |
| assert_create_raw_processes_allowed_matches!(policy, allowed2, Ok(())); |
| assert_create_raw_processes_disallowed!(policy, Moniker::root()); |
| assert_create_raw_processes_disallowed!(policy, allowed1.parent().unwrap()); |
| assert_create_raw_processes_disallowed!( |
| policy, |
| allowed1.child(ChildName::try_from("baz").unwrap()) |
| ); |
| } |
| |
| #[test] |
| fn scoped_policy_checker_main_process_critical_allowed() { |
| macro_rules! assert_critical_allowed_matches { |
| ($policy:expr, $moniker:expr, $expected:pat) => { |
| let result = ScopedPolicyChecker::new($policy.clone(), $moniker.clone()) |
| .main_process_critical_allowed(); |
| assert_matches!(result, $expected); |
| }; |
| } |
| macro_rules! assert_critical_disallowed { |
| ($policy:expr, $moniker:expr) => { |
| assert_critical_allowed_matches!( |
| $policy, |
| $moniker, |
| Err(PolicyError::JobPolicyDisallowed { .. }) |
| ); |
| }; |
| } |
| let policy = Arc::new(SecurityPolicy::default()); |
| assert_critical_disallowed!(policy, Moniker::root()); |
| assert_critical_disallowed!(policy, Moniker::try_from(vec!["foo"]).unwrap()); |
| |
| let allowed1 = Moniker::try_from(vec!["foo", "bar"]).unwrap(); |
| let allowed2 = Moniker::try_from(vec!["baz", "fiz"]).unwrap(); |
| let policy = Arc::new(SecurityPolicy { |
| job_policy: JobPolicyAllowlists { |
| ambient_mark_vmo_exec: vec![ |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed1), |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed2), |
| ], |
| main_process_critical: vec![ |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed1), |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed2), |
| ], |
| create_raw_processes: vec![ |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed1), |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed2), |
| ], |
| }, |
| capability_policy: HashMap::new(), |
| debug_capability_policy: HashMap::new(), |
| child_policy: ChildPolicyAllowlists { reboot_on_terminate: vec![] }, |
| }); |
| assert_critical_allowed_matches!(policy, allowed1, Ok(())); |
| assert_critical_allowed_matches!(policy, allowed2, Ok(())); |
| assert_critical_disallowed!(policy, Moniker::root()); |
| assert_critical_disallowed!(policy, allowed1.parent().unwrap()); |
| assert_critical_disallowed!(policy, allowed1.child(ChildName::try_from("baz").unwrap())); |
| } |
| |
| #[test] |
| fn scoped_policy_checker_reboot_policy_allowed() { |
| macro_rules! assert_reboot_allowed_matches { |
| ($policy:expr, $moniker:expr, $expected:pat) => { |
| let result = GlobalPolicyChecker::new($policy.clone()) |
| .reboot_on_terminate_allowed(&$moniker); |
| assert_matches!(result, $expected); |
| }; |
| } |
| macro_rules! assert_reboot_disallowed { |
| ($policy:expr, $moniker:expr) => { |
| assert_reboot_allowed_matches!( |
| $policy, |
| $moniker, |
| Err(PolicyError::ChildPolicyDisallowed { .. }) |
| ); |
| }; |
| } |
| |
| // Empty policy and enabled. |
| let policy = Arc::new(SecurityPolicy::default()); |
| assert_reboot_disallowed!(policy, Moniker::root()); |
| assert_reboot_disallowed!(policy, Moniker::try_from(vec!["foo"]).unwrap()); |
| |
| // Nonempty policy. |
| let allowed1 = Moniker::try_from(vec!["foo", "bar"]).unwrap(); |
| let allowed2 = Moniker::try_from(vec!["baz", "fiz"]).unwrap(); |
| let policy = Arc::new(SecurityPolicy { |
| job_policy: JobPolicyAllowlists { |
| ambient_mark_vmo_exec: vec![], |
| main_process_critical: vec![], |
| create_raw_processes: vec![], |
| }, |
| capability_policy: HashMap::new(), |
| debug_capability_policy: HashMap::new(), |
| child_policy: ChildPolicyAllowlists { |
| reboot_on_terminate: vec![ |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed1), |
| AllowlistEntryBuilder::build_exact_from_moniker(&allowed2), |
| ], |
| }, |
| }); |
| assert_reboot_allowed_matches!(policy, allowed1, Ok(())); |
| assert_reboot_allowed_matches!(policy, allowed2, Ok(())); |
| assert_reboot_disallowed!(policy, Moniker::root()); |
| assert_reboot_disallowed!(policy, allowed1.parent().unwrap()); |
| assert_reboot_disallowed!(policy, allowed1.child(ChildName::try_from("baz").unwrap())); |
| } |
| } |