blob: 2ff6e843f7a963a9b63dd33e76642f1ed1e81e5d [file] [log] [blame]
// 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::CapabilitySource,
config::{CapabilityAllowlistKey, CapabilityAllowlistSource, RuntimeConfig},
model::error::ModelError,
},
moniker::{AbsoluteMoniker, ChildMoniker, ExtendedMoniker},
std::sync::{Arc, Weak},
thiserror::Error,
};
/// Errors returned by the PolicyChecker and the ScopedPolicyChecker.
#[derive(Debug, Clone, Error)]
pub enum PolicyError {
#[error("security policy was unavailable to check")]
PolicyUnavailable,
#[error("security policy disallows \"{policy}\" job policy for \"{moniker}\"")]
JobPolicyDisallowed { policy: String, moniker: AbsoluteMoniker },
#[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: AbsoluteMoniker,
},
}
impl PolicyError {
fn job_policy_disallowed(policy: impl Into<String>, moniker: &AbsoluteMoniker) -> Self {
PolicyError::JobPolicyDisallowed { policy: policy.into(), moniker: moniker.clone() }
}
fn capability_use_disallowed(
cap: impl Into<String>,
source_moniker: &ExtendedMoniker,
target_moniker: &AbsoluteMoniker,
) -> Self {
PolicyError::CapabilityUseDisallowed {
cap: cap.into(),
source_moniker: source_moniker.clone(),
target_moniker: target_moniker.clone(),
}
}
}
/// Evaluates security policy globally across the entire Model and all realms.
/// This is used to enforce runtime capability routing restrictions across all
/// realms to prevent high privilleged capabilities from being routed to
/// components outside of the list defined in the runtime configs security
/// policy.
pub struct GlobalPolicyChecker {
/// The runtime configuration containing the security policy to apply.
config: Arc<RuntimeConfig>,
}
impl GlobalPolicyChecker {
/// Constructs a new PolicyChecker object configured by the
/// RuntimeConfig::SecurityPolicy.
pub fn new(config: Arc<RuntimeConfig>) -> Self {
Self { config: config }
}
/// Absolute monikers contain instance_id. This change normalizes all
/// incoming instance identifiers to 0 so for example
/// /foo:1/bar:0 -> /foo:0/bar:0.
fn strip_moniker_instance_id(moniker: &AbsoluteMoniker) -> AbsoluteMoniker {
let mut normalized_children = Vec::with_capacity(moniker.path().len());
for child in moniker.path().iter() {
normalized_children.push(ChildMoniker::new(
child.name().to_string(),
child.collection().map(String::from),
0,
));
}
AbsoluteMoniker::new(normalized_children)
}
/// 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>(
&self,
capability_source: &'a CapabilitySource,
target_moniker: &'a AbsoluteMoniker,
) -> Result<(), ModelError> {
let target_moniker = Self::strip_moniker_instance_id(&target_moniker);
let policy_key = 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, realm } => CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentInstance(
realm.upgrade()?.abs_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, scope_moniker } => CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentInstance(scope_moniker.clone()),
source_name: capability.source_name().clone(),
source: CapabilityAllowlistSource::Framework,
capability: capability.type_name(),
},
CapabilitySource::Capability { source_capability, realm } => CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentInstance(
realm.upgrade()?.abs_moniker.clone(),
),
source_name: source_capability
.source_name()
.ok_or(PolicyError::InvalidCapabilitySource)?
.clone(),
source: CapabilityAllowlistSource::Capability,
capability: source_capability.type_name(),
},
};
match self.config.security_policy.capability_policy.get(&policy_key) {
Some(allowed_monikers) => match allowed_monikers.get(&target_moniker) {
Some(_) => Ok(()),
None => Err(ModelError::PolicyError {
err: PolicyError::capability_use_disallowed(
policy_key.source_name.str(),
&policy_key.source_moniker,
&target_moniker,
),
}),
},
None => Ok(()),
}
}
}
/// Evaluates security policy relative to a specific Realm (based on that Realm's AbsoluteMoniker).
pub struct ScopedPolicyChecker {
/// The runtime configuration containing the security policy to apply.
config: Weak<RuntimeConfig>,
/// The absolute moniker of the realm that policy will be evaluated for.
moniker: AbsoluteMoniker,
}
impl ScopedPolicyChecker {
pub fn new(config: Weak<RuntimeConfig>, moniker: AbsoluteMoniker) -> Self {
ScopedPolicyChecker { config, moniker }
}
// This interface is super simple for now since there's only two 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> {
let config = self.config.upgrade().ok_or(PolicyError::PolicyUnavailable)?;
if config.security_policy.job_policy.ambient_mark_vmo_exec.contains(&self.moniker) {
Ok(())
} else {
Err(PolicyError::job_policy_disallowed("ambient_mark_vmo_exec", &self.moniker))
}
}
pub fn main_process_critical_allowed(&self) -> Result<(), PolicyError> {
let config = self.config.upgrade().ok_or(PolicyError::PolicyUnavailable)?;
if config.security_policy.job_policy.main_process_critical.contains(&self.moniker) {
Ok(())
} else {
Err(PolicyError::job_policy_disallowed("main_process_critical", &self.moniker))
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
capability::{ComponentCapability, InternalCapability},
config::{JobPolicyAllowlists, SecurityPolicy},
model::{
environment::{Environment, RunnerRegistry},
realm::Realm,
resolver::ResolverRegistry,
},
},
cm_rust::*,
matches::assert_matches,
moniker::ChildMoniker,
std::{collections::HashMap, collections::HashSet, iter::FromIterator, sync::Arc},
};
/// Creates a RuntimeConfig based on the capability allowlist entries provided during
/// construction.
struct CapabilityAllowlistConfigBuilder {
capability_policy: HashMap<CapabilityAllowlistKey, HashSet<AbsoluteMoniker>>,
}
impl CapabilityAllowlistConfigBuilder {
pub fn new() -> Self {
Self { capability_policy: HashMap::new() }
}
/// Add a new entry to the configuration.
pub fn add<'a>(
&'a mut self,
key: CapabilityAllowlistKey,
value: Vec<AbsoluteMoniker>,
) -> &'a mut Self {
let value_set = HashSet::from_iter(value.iter().cloned());
self.capability_policy.insert(key, value_set);
self
}
/// Creates a configuration from the provided policies.
pub fn build(&self) -> Arc<RuntimeConfig> {
let config = Arc::new(RuntimeConfig {
security_policy: SecurityPolicy {
job_policy: JobPolicyAllowlists {
ambient_mark_vmo_exec: vec![],
main_process_critical: vec![],
},
capability_policy: self.capability_policy.clone(),
},
..Default::default()
});
config
}
}
#[test]
fn scoped_policy_checker_vmex() {
macro_rules! assert_vmex_allowed_matches {
($config:expr, $moniker:expr, $expected:pat) => {
let result = ScopedPolicyChecker::new($config.clone(), $moniker.clone())
.ambient_mark_vmo_exec_allowed();
assert_matches!(result, $expected);
};
}
macro_rules! assert_vmex_disallowed {
($config:expr, $moniker:expr) => {
assert_vmex_allowed_matches!(
$config,
$moniker,
Err(PolicyError::JobPolicyDisallowed { .. })
);
};
}
let strong_config = Arc::new(RuntimeConfig::default());
let config = Arc::downgrade(&strong_config);
assert_vmex_disallowed!(config, AbsoluteMoniker::root());
assert_vmex_disallowed!(config, AbsoluteMoniker::from(vec!["foo:0"]));
let allowed1 = AbsoluteMoniker::from(vec!["foo:0", "bar:0"]);
let allowed2 = AbsoluteMoniker::from(vec!["baz:0", "fiz:0"]);
let strong_config = Arc::new(RuntimeConfig {
security_policy: SecurityPolicy {
job_policy: JobPolicyAllowlists {
ambient_mark_vmo_exec: vec![allowed1.clone(), allowed2.clone()],
main_process_critical: vec![allowed1.clone(), allowed2.clone()],
},
capability_policy: HashMap::new(),
},
..Default::default()
});
let config = Arc::downgrade(&strong_config);
assert_vmex_allowed_matches!(config, allowed1, Ok(()));
assert_vmex_allowed_matches!(config, allowed2, Ok(()));
assert_vmex_disallowed!(config, AbsoluteMoniker::root());
assert_vmex_disallowed!(config, allowed1.parent().unwrap());
assert_vmex_disallowed!(config, allowed1.child(ChildMoniker::from("baz:0")));
drop(strong_config);
assert_vmex_allowed_matches!(config, allowed1, Err(PolicyError::PolicyUnavailable));
assert_vmex_allowed_matches!(config, allowed2, Err(PolicyError::PolicyUnavailable));
}
#[test]
fn scoped_policy_checker_critical_allowed() {
macro_rules! assert_critical_allowed_matches {
($config:expr, $moniker:expr, $expected:pat) => {
let result = ScopedPolicyChecker::new($config.clone(), $moniker.clone())
.main_process_critical_allowed();
assert_matches!(result, $expected);
};
}
macro_rules! assert_critical_disallowed {
($config:expr, $moniker:expr) => {
assert_critical_allowed_matches!(
$config,
$moniker,
Err(PolicyError::JobPolicyDisallowed { .. })
);
};
}
let strong_config = Arc::new(RuntimeConfig::default());
let config = Arc::downgrade(&strong_config);
assert_critical_disallowed!(config, AbsoluteMoniker::root());
assert_critical_disallowed!(config, AbsoluteMoniker::from(vec!["foo:0"]));
let allowed1 = AbsoluteMoniker::from(vec!["foo:0", "bar:0"]);
let allowed2 = AbsoluteMoniker::from(vec!["baz:0", "fiz:0"]);
let strong_config = Arc::new(RuntimeConfig {
security_policy: SecurityPolicy {
job_policy: JobPolicyAllowlists {
ambient_mark_vmo_exec: vec![allowed1.clone(), allowed2.clone()],
main_process_critical: vec![allowed1.clone(), allowed2.clone()],
},
capability_policy: HashMap::new(),
},
..Default::default()
});
let config = Arc::downgrade(&strong_config);
assert_critical_allowed_matches!(config, allowed1, Ok(()));
assert_critical_allowed_matches!(config, allowed2, Ok(()));
assert_critical_disallowed!(config, AbsoluteMoniker::root());
assert_critical_disallowed!(config, allowed1.parent().unwrap());
assert_critical_disallowed!(config, allowed1.child(ChildMoniker::from("baz:0")));
drop(strong_config);
assert_critical_allowed_matches!(config, allowed1, Err(PolicyError::PolicyUnavailable));
assert_critical_allowed_matches!(config, allowed2, Err(PolicyError::PolicyUnavailable));
}
#[test]
fn global_policy_checker_can_route_capability_framework_cap() {
let mut config_builder = CapabilityAllowlistConfigBuilder::new();
config_builder.add(
CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentInstance(AbsoluteMoniker::from(vec![
"foo:0", "bar:0",
])),
source_name: CapabilityName::from("running"),
source: CapabilityAllowlistSource::Framework,
capability: CapabilityTypeName::Event,
},
vec![
AbsoluteMoniker::from(vec!["foo:0", "bar:0"]),
AbsoluteMoniker::from(vec!["foo:0", "bar:0", "baz:0"]),
],
);
let global_policy_checker = GlobalPolicyChecker::new(config_builder.build());
let event_capability = CapabilitySource::Framework {
capability: InternalCapability::Event(CapabilityName::from("running")),
scope_moniker: AbsoluteMoniker::from(vec!["foo:0", "bar:0"]),
};
let valid_path_0 = AbsoluteMoniker::from(vec!["foo:0", "bar:0"]);
let valid_path_1 = AbsoluteMoniker::from(vec!["foo:0", "bar:0", "baz:0"]);
let invalid_path_0 = AbsoluteMoniker::from(vec!["foobar:0"]);
let invalid_path_1 = AbsoluteMoniker::from(vec!["foo:0", "bar:0", "foobar:0"]);
assert_matches!(
global_policy_checker.can_route_capability(&event_capability, &valid_path_0),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&event_capability, &valid_path_1),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&event_capability, &invalid_path_0),
Err(_)
);
assert_matches!(
global_policy_checker.can_route_capability(&event_capability, &invalid_path_1),
Err(_)
);
}
#[test]
fn global_policy_checker_can_route_capability_namespace_cap() {
let mut config_builder = CapabilityAllowlistConfigBuilder::new();
config_builder.add(
CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentManager,
source_name: CapabilityName::from("fuchsia.kernel.RootResource"),
source: CapabilityAllowlistSource::Self_,
capability: CapabilityTypeName::Protocol,
},
vec![
AbsoluteMoniker::from(vec!["root:0"]),
AbsoluteMoniker::from(vec!["root:0", "bootstrap:0"]),
AbsoluteMoniker::from(vec!["root:0", "core:0"]),
],
);
let global_policy_checker = GlobalPolicyChecker::new(config_builder.build());
let protocol_capability = CapabilitySource::Namespace {
capability: ComponentCapability::Use(UseDecl::Protocol(UseProtocolDecl {
source: UseSource::Parent,
source_name: "fuchsia.kernel.RootResource".into(),
target_path: "/svc/fuchsia.kernel.RootResource".parse().unwrap(),
})),
};
let valid_path_0 = AbsoluteMoniker::from(vec!["root:0"]);
let valid_path_1 = AbsoluteMoniker::from(vec!["root:0", "bootstrap:0"]);
let valid_path_2 = AbsoluteMoniker::from(vec!["root:0", "core:0"]);
let invalid_path_0 = AbsoluteMoniker::from(vec!["foobar:0"]);
let invalid_path_1 = AbsoluteMoniker::from(vec!["foo:0", "bar:0", "foobar:0"]);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &valid_path_0),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &valid_path_1),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &valid_path_2),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &invalid_path_0),
Err(_)
);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &invalid_path_1),
Err(_)
);
}
#[test]
fn global_policy_checker_can_route_capability_component_cap() {
let mut config_builder = CapabilityAllowlistConfigBuilder::new();
config_builder.add(
CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentInstance(AbsoluteMoniker::from(vec![
"foo:0",
])),
source_name: CapabilityName::from("fuchsia.foo.FooBar"),
source: CapabilityAllowlistSource::Self_,
capability: CapabilityTypeName::Protocol,
},
vec![
AbsoluteMoniker::from(vec!["foo:0"]),
AbsoluteMoniker::from(vec!["root:0", "bootstrap:0"]),
AbsoluteMoniker::from(vec!["root:0", "core:0"]),
],
);
let global_policy_checker = GlobalPolicyChecker::new(config_builder.build());
// Create a fake realm.
let resolver = ResolverRegistry::new();
let root_component_url = "test:///foo".to_string();
let mut realm = Realm::new_root_realm(
Environment::new_root(RunnerRegistry::default(), resolver),
Weak::new(),
Weak::new(),
root_component_url,
);
realm.abs_moniker = AbsoluteMoniker::from(vec!["foo:0"]);
let realm = Arc::new(realm);
let weak_realm = realm.as_weak();
let protocol_capability = CapabilitySource::Component {
capability: ComponentCapability::Use(UseDecl::Protocol(UseProtocolDecl {
source: UseSource::Parent,
source_name: "fuchsia.foo.FooBar".into(),
target_path: "/svc/fuchsia.foo.FooBar".parse().unwrap(),
})),
realm: weak_realm,
};
let valid_path_0 = AbsoluteMoniker::from(vec!["root:0", "bootstrap:0"]);
let valid_path_1 = AbsoluteMoniker::from(vec!["root:0", "core:0"]);
let invalid_path_0 = AbsoluteMoniker::from(vec!["foobar:0"]);
let invalid_path_1 = AbsoluteMoniker::from(vec!["foo:0", "bar:0", "foobar:0"]);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &valid_path_0),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &valid_path_1),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &invalid_path_0),
Err(_)
);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &invalid_path_1),
Err(_)
);
}
#[test]
fn global_policy_checker_can_route_capability_capability_cap() {
let mut config_builder = CapabilityAllowlistConfigBuilder::new();
config_builder.add(
CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentInstance(AbsoluteMoniker::from(vec![
"foo:0",
])),
source_name: CapabilityName::from("fuchsia.foo.FooBar"),
source: CapabilityAllowlistSource::Capability,
capability: CapabilityTypeName::Protocol,
},
vec![
AbsoluteMoniker::from(vec!["foo:0"]),
AbsoluteMoniker::from(vec!["root:0", "bootstrap:0"]),
AbsoluteMoniker::from(vec!["root:0", "core:0"]),
],
);
let global_policy_checker = GlobalPolicyChecker::new(config_builder.build());
// Create a fake realm.
let resolver = ResolverRegistry::new();
let root_component_url = "test:///foo".to_string();
let mut realm = Realm::new_root_realm(
Environment::new_root(RunnerRegistry::default(), resolver),
Weak::new(),
Weak::new(),
root_component_url,
);
realm.abs_moniker = AbsoluteMoniker::from(vec!["foo:0"]);
let realm = Arc::new(realm);
let weak_realm = realm.as_weak();
let protocol_capability = CapabilitySource::Capability {
source_capability: ComponentCapability::Use(UseDecl::Protocol(UseProtocolDecl {
source: UseSource::Parent,
source_name: "fuchsia.foo.FooBar".into(),
target_path: "/svc/fuchsia.foo.FooBar".parse().unwrap(),
})),
realm: weak_realm,
};
let valid_path_0 = AbsoluteMoniker::from(vec!["root:0", "bootstrap:0"]);
let valid_path_1 = AbsoluteMoniker::from(vec!["root:0", "core:0"]);
let invalid_path_0 = AbsoluteMoniker::from(vec!["foobar:0"]);
let invalid_path_1 = AbsoluteMoniker::from(vec!["foo:0", "bar:0", "foobar:0"]);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &valid_path_0),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &valid_path_1),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &invalid_path_0),
Err(_)
);
assert_matches!(
global_policy_checker.can_route_capability(&protocol_capability, &invalid_path_1),
Err(_)
);
}
#[test]
fn global_policy_checker_can_route_capability_builtin_cap() {
let mut config_builder = CapabilityAllowlistConfigBuilder::new();
config_builder.add(
CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentManager,
source_name: CapabilityName::from("hub"),
source: CapabilityAllowlistSource::Self_,
capability: CapabilityTypeName::Directory,
},
vec![
AbsoluteMoniker::from(vec!["root:0"]),
AbsoluteMoniker::from(vec!["root:0", "core:0"]),
],
);
let global_policy_checker = GlobalPolicyChecker::new(config_builder.build());
let dir_capability = CapabilitySource::Builtin {
capability: InternalCapability::Directory(CapabilityName::from("hub")),
};
let valid_path_0 = AbsoluteMoniker::from(vec!["root:0"]);
let valid_path_1 = AbsoluteMoniker::from(vec!["root:0", "core:0"]);
let invalid_path_0 = AbsoluteMoniker::from(vec!["foobar:0"]);
let invalid_path_1 = AbsoluteMoniker::from(vec!["foo:0", "bar:0", "foobar:0"]);
assert_matches!(
global_policy_checker.can_route_capability(&dir_capability, &valid_path_0),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&dir_capability, &valid_path_1),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&dir_capability, &invalid_path_0),
Err(_)
);
assert_matches!(
global_policy_checker.can_route_capability(&dir_capability, &invalid_path_1),
Err(_)
);
}
#[test]
fn global_policy_checker_can_route_capability_with_instance_ids_cap() {
let mut config_builder = CapabilityAllowlistConfigBuilder::new();
config_builder.add(
CapabilityAllowlistKey {
source_moniker: ExtendedMoniker::ComponentManager,
source_name: CapabilityName::from("hub"),
source: CapabilityAllowlistSource::Self_,
capability: CapabilityTypeName::Directory,
},
vec![
AbsoluteMoniker::from(vec!["root:0"]),
AbsoluteMoniker::from(vec!["root:0", "core:0"]),
],
);
let global_policy_checker = GlobalPolicyChecker::new(config_builder.build());
let dir_capability = CapabilitySource::Builtin {
capability: InternalCapability::Directory(CapabilityName::from("hub")),
};
let valid_path_0 = AbsoluteMoniker::from(vec!["root:1"]);
let valid_path_1 = AbsoluteMoniker::from(vec!["root:5", "core:3"]);
let invalid_path_0 = AbsoluteMoniker::from(vec!["foobar:0"]);
let invalid_path_1 = AbsoluteMoniker::from(vec!["foo:0", "bar:2", "foobar:0"]);
assert_matches!(
global_policy_checker.can_route_capability(&dir_capability, &valid_path_0),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&dir_capability, &valid_path_1),
Ok(())
);
assert_matches!(
global_policy_checker.can_route_capability(&dir_capability, &invalid_path_0),
Err(_)
);
assert_matches!(
global_policy_checker.can_route_capability(&dir_capability, &invalid_path_1),
Err(_)
);
}
}