diff --git a/docs/concepts.md b/docs/concepts.md index 332fabfce6f..2612bbac4b6 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -249,12 +249,28 @@ allowed (via two uni-directional Channels) if `(L_a ⊑ L_b) ∧ (L_b ⊑ L_a) ⇒ L_a = L_b`, i.e. if `a` and `b` have identical secrecy and integrity. +#### Downgrades + +A Node may have the ability to remove one or more secrecy tags +(**declassification**) or add one or more integrity tags (**endorsement**). Both +of these operations are instances of **downgrade** operations (which is a more +general concept). + +The set of tags that can be downgraded by a Node is determined by the Oak +Runtime based on the initial or current state of the Node. For instance, the Oak +Runtime grants the capability to declassify user tags to each instance of gRPC +Server Node, which is trusted to only use it in order to declassify data for the +user that is in fact currently authenticated over a gRPC connection. + +#### References + More details on Information Flow Control may be found in the following references: - [Information Flow Control for Standard OS Abstractions](https://pdos.csail.mit.edu/papers/flume-sosp07.pdf) - [Flow-Limited Authorization](https://www.cs.cornell.edu/andru/papers/flam/flam-csf15.pdf) - [Integrity Considerations for Secure Computer Systems](http://seclab.cs.ucdavis.edu/projects/history/papers/biba75.pdf) +- [Protecting Privacy using the Decentralized Label Model](https://www.cs.cornell.edu/andru/papers/iflow-tosem.pdf) ### gRPC and user labels diff --git a/oak/server/rust/oak_runtime/src/runtime/mod.rs b/oak/server/rust/oak_runtime/src/runtime/mod.rs index 78245cb7722..5b83b3ba9b7 100644 --- a/oak/server/rust/oak_runtime/src/runtime/mod.rs +++ b/oak/server/rust/oak_runtime/src/runtime/mod.rs @@ -22,12 +22,15 @@ use crate::{ }; use core::sync::atomic::{AtomicBool, AtomicU64, Ordering::SeqCst}; use itertools::Itertools; -use log::{debug, error, info, trace, warn}; -use oak_abi::{label::Label, ChannelReadStatus, OakStatus}; +use log::{debug, error, info, trace}; +use oak_abi::{ + label::{Label, Tag}, + ChannelReadStatus, OakStatus, +}; use prometheus::proto::MetricFamily; use rand::RngCore; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, fmt::Write, string::String, sync::{Arc, Mutex, RwLock}, @@ -66,6 +69,9 @@ struct NodeInfo { /// See https://github.com/project-oak/oak/blob/master/docs/concepts.md#labels label: Label, + /// The downgrading privilege of this Node. + privilege: NodePrivilege, + /// Map of ABI handles to channels. abi_handles: HashMap, @@ -75,6 +81,18 @@ struct NodeInfo { join_handle: Option>, } +/// The downgrading (declassification + endorsement) privilege associated with a Node instance. +/// +/// See https://github.com/project-oak/oak/blob/master/docs/concepts.md#downgrades +#[derive(Debug, Default, Clone)] +pub struct NodePrivilege { + /// Tags that may be declassified (removed from the secrecy component of a label) by the Node. + can_declassify_secrecy_tags: HashSet, + + /// Tags that may be endorsed (added to the integrity component of a label) by the Node. + can_endorse_integrity_tags: HashSet, +} + impl std::fmt::Debug for NodeInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -304,6 +322,7 @@ impl RuntimeProxy { proxy.node_id, "implicit.initial", &Label::public_trusted(), + &NodePrivilege::default(), ); proxy } @@ -688,6 +707,41 @@ impl Runtime { node_info.label.clone() } + /// Returns a version of the [`Label`] of the provided Node, taking into account all the + /// [downgrade privilege](NodeInfo::privilege) that the node possesses. + fn get_node_downgraded_label(&self, node_id: NodeId) -> Label { + // Original (static) Node label. + let node_label = self.get_node_label(node_id); + // Retrieve the set of tags that the node may downgrade. + let node_privilege = self.get_node_privilege(node_id); + Label { + // Remove all the secrecy tags that the Node may declassify. + secrecy_tags: node_label + .secrecy_tags + .iter() + .filter(|t| !node_privilege.can_declassify_secrecy_tags.contains(t)) + .cloned() + .collect(), + // Add all the integrity tags that the Node may endorse. + integrity_tags: node_label + .integrity_tags + .iter() + .chain(node_privilege.can_endorse_integrity_tags.iter()) + .cloned() + .collect(), + } + } + + /// Returns a clone of the [`NodePrivilege`] of the provided Node. + fn get_node_privilege(&self, node_id: NodeId) -> NodePrivilege { + let node_infos = self + .node_infos + .read() + .expect("could not acquire lock on node_infos"); + let node_info = node_infos.get(&node_id).expect("invalid node_id"); + node_info.privilege.clone() + } + /// Returns a clone of the [`Label`] associated with the provided reader `channel_half`. /// /// Returns an error if `channel_half` is not a valid read half. @@ -702,62 +756,66 @@ impl Runtime { with_writer_channel(channel_half, |channel| Ok(channel.label.clone())) } - /// Returns whether the given Node is allowed to read from the provided channel, according to - /// their respective [`Label`]s. + /// Returns whether the given Node is allowed to read from the provided channel read half, + /// according to their respective [`Label`]s. fn validate_can_read_from_channel( &self, node_id: NodeId, channel_half: &ChannelHalf, ) -> Result<(), OakStatus> { - trace!( - "{:?}: validating readability of {:?}", - node_id, - channel_half - ); - - let node_label = self.get_node_label(node_id); let channel_label = self.get_reader_channel_label(&channel_half)?; + self.validate_can_read_from_label(node_id, &channel_label) + } + + /// Returns whether the given Node is allowed to read from an entity with the provided + /// [`Label`], taking into account any dowgrades it may be capable of. + fn validate_can_read_from_label( + &self, + node_id: NodeId, + label: &Label, + ) -> Result<(), OakStatus> { + let downgraded_node_label = self.get_node_downgraded_label(node_id); trace!( - "{:?}: node_label={:?}, channel_label={:?}", + "{:?}: can {:?} read from {:?}?", node_id, - node_label, - channel_label + downgraded_node_label, + label ); - if channel_label.flows_to(&node_label) { - trace!("{:?}: can read from channel {:?}", node_id, channel_half); + if label.flows_to(&downgraded_node_label) { + trace!("{:?}: can read from {:?}", node_id, label); Ok(()) } else { - debug!("{:?}: cannot read from channel {:?}", node_id, channel_half); + debug!("{:?}: cannot read from {:?}", node_id, label); Err(OakStatus::ErrPermissionDenied) } } - /// Returns whether the given Node is allowed to write to the provided channel, according to - /// their respective [`Label`]s. + /// Returns whether the given Node is allowed to write to the provided channel write half, + /// according to their respective [`Label`]s. fn validate_can_write_to_channel( &self, node_id: NodeId, channel_half: &ChannelHalf, ) -> Result<(), OakStatus> { - trace!( - "{:?}: validating writability of {:?}", - node_id, - channel_half - ); - - let node_label = self.get_node_label(node_id); let channel_label = self.get_writer_channel_label(&channel_half)?; + self.validate_can_write_to_label(node_id, &channel_label) + } + + /// Returns whether the given Node is allowed to write to an entity with the provided [`Label`], + /// taking into account any dowgrades it may be capable of. + fn validate_can_write_to_label(&self, node_id: NodeId, label: &Label) -> Result<(), OakStatus> { + let downgraded_node_label = self.get_node_downgraded_label(node_id); trace!( - "{:?}: node_label={:?}, channel_label={:?}", + "{:?}: can {:?} write to {:?}?", node_id, - node_label, - channel_label + downgraded_node_label, + label ); - if node_label.flows_to(&channel_label) { - trace!("{:?}: can write to channel {:?}", node_id, channel_half); + if downgraded_node_label.flows_to(&label) { + trace!("{:?}: can write to {:?}", node_id, label); Ok(()) } else { - debug!("{:?}: cannot write to channel {:?}", node_id, channel_half); + debug!("{:?}: cannot write to {:?}", node_id, label); Err(OakStatus::ErrPermissionDenied) } } @@ -770,14 +828,7 @@ impl Runtime { node_id: NodeId, label: &Label, ) -> Result<(oak_abi::Handle, oak_abi::Handle), OakStatus> { - let node_label = self.get_node_label(node_id); - if !node_label.flows_to(label) { - warn!( - "channel_create: label {:?} does not flow to label {:?}", - node_label, label - ); - return Err(OakStatus::ErrPermissionDenied); - } + self.validate_can_write_to_label(node_id, label)?; // First get a pair of `ChannelHalf` objects. let channel_id = self.next_channel_id.fetch_add(1, SeqCst); let channel = Channel::new(channel_id, label); @@ -1143,18 +1194,10 @@ impl Runtime { if self.is_terminating() { return Err(OakStatus::ErrTerminated); } + self.validate_can_write_to_label(node_id, label)?; let reader = self.abi_to_read_half(node_id, initial_handle)?; - let node_label = self.get_node_label(node_id); - if !node_label.flows_to(label) { - warn!( - "node_create: label {:?} does not flow to label {:?}", - node_label, label - ); - return Err(OakStatus::ErrPermissionDenied); - } - let config = self .configuration .nodes @@ -1169,7 +1212,12 @@ impl Runtime { config.node_subname(entrypoint), new_node_id.0 ); - self.node_configure_instance(new_node_id, &new_node_name, label); + self.node_configure_instance( + new_node_id, + &new_node_name, + label, + &NodePrivilege::default(), + ); let initial_handle = new_node_proxy .runtime .new_abi_handle(new_node_proxy.node_id, reader.clone()); @@ -1234,12 +1282,19 @@ impl Runtime { } /// Configure data structures for a Node instance. - fn node_configure_instance(&self, node_id: NodeId, node_name: &str, label: &Label) { + fn node_configure_instance( + &self, + node_id: NodeId, + node_name: &str, + label: &Label, + privilege: &NodePrivilege, + ) { self.add_node_info( node_id, NodeInfo { name: node_name.to_string(), label: label.clone(), + privilege: privilege.clone(), abi_handles: HashMap::new(), join_handle: None, }, diff --git a/oak/server/rust/oak_runtime/src/runtime/tests.rs b/oak/server/rust/oak_runtime/src/runtime/tests.rs index c0aec885935..c4a94c7a189 100644 --- a/oak/server/rust/oak_runtime/src/runtime/tests.rs +++ b/oak/server/rust/oak_runtime/src/runtime/tests.rs @@ -15,6 +15,7 @@ // use super::*; +use maplit::hashset; use std::sync::Once; static LOG_INIT_ONCE: Once = Once::new(); @@ -30,12 +31,12 @@ type NodeBody = dyn Fn(RuntimeProxy) -> Result<(), OakStatus> + Send + Sync; /// Runs the provided function as if it were the body of a [`Node`] implementation, which is /// instantiated by the [`Runtime`] with the provided [`Label`]. -fn run_node_body(node_label: &Label, node_body: Box) { +fn run_node_body(node_label: &Label, node_privilege: &NodePrivilege, node_body: Box) { init_logging(); let configuration = crate::runtime::Configuration { - nodes: maplit::hashmap![ + nodes: maplit::hashmap! { "log".to_string() => crate::node::Configuration::LogNode, - ], + }, entry_module: "test_module".to_string(), entrypoint: "test_function".to_string(), }; @@ -57,9 +58,12 @@ fn run_node_body(node_label: &Label, node_body: Box) { let test_proxy = proxy.clone().runtime.proxy_for_new_node(); let new_node_id = test_proxy.node_id; let new_node_name = format!("TestNode({})", new_node_id.0); - proxy - .runtime - .node_configure_instance(new_node_id, "test_module.test_function", &node_label); + proxy.runtime.node_configure_instance( + new_node_id, + "test_module.test_function", + node_label, + node_privilege, + ); let node_instance = TestNode { node_body }; info!("Start test Node instance"); @@ -103,6 +107,7 @@ fn panic_check() { let label = test_label(); run_node_body( &label, + &NodePrivilege::default(), Box::new(|_runtime| { panic!("testing that panic works"); }), @@ -116,6 +121,7 @@ fn create_channel_same_label_ok() { let label_clone = label.clone(); run_node_body( &label, + &NodePrivilege::default(), Box::new(move |runtime| { // Attempt to perform an operation that requires the [`Runtime`] to have created an // appropriate [`NodeInfo`] instance. @@ -133,18 +139,19 @@ fn create_channel_same_label_ok() { /// a covert channel to exfiltrate secret data. #[test] fn create_channel_less_secret_label_err() { + let tag_0 = oak_abi::label::authorization_bearer_token_hmac_tag(&[1, 1, 1]); + let tag_1 = oak_abi::label::authorization_bearer_token_hmac_tag(&[2, 2, 2]); let initial_label = Label { - secrecy_tags: vec![oak_abi::label::authorization_bearer_token_hmac_tag(&[ - 1, 1, 1, - ])], + secrecy_tags: vec![tag_0, tag_1.clone()], integrity_tags: vec![], }; let less_secret_label = Label { - secrecy_tags: vec![], + secrecy_tags: vec![tag_1], integrity_tags: vec![], }; run_node_body( &initial_label, + &NodePrivilege::default(), Box::new(move |runtime| { let result = runtime.channel_create(&less_secret_label); assert_eq!(Err(OakStatus::ErrPermissionDenied), result); @@ -153,6 +160,35 @@ fn create_channel_less_secret_label_err() { ); } +/// Create a test Node that creates a Channel with a less secret label and succeeds, because the +/// node is granted the ability to downgrade the removed secrecy tag. +#[test] +fn create_channel_less_secret_label_downgrade_ok() { + let tag_0 = oak_abi::label::authorization_bearer_token_hmac_tag(&[1, 1, 1]); + let tag_1 = oak_abi::label::authorization_bearer_token_hmac_tag(&[2, 2, 2]); + let initial_label = Label { + secrecy_tags: vec![tag_0.clone(), tag_1.clone()], + integrity_tags: vec![], + }; + let less_secret_label = Label { + secrecy_tags: vec![tag_1], + integrity_tags: vec![], + }; + run_node_body( + &initial_label, + // Grant this node the privilege to declassify `tag_0`. + &NodePrivilege { + can_declassify_secrecy_tags: hashset! { tag_0 }, + can_endorse_integrity_tags: hashset! {}, + }, + Box::new(move |runtime| { + let result = runtime.channel_create(&less_secret_label); + assert_eq!(true, result.is_ok()); + Ok(()) + }), + ); +} + /// Create a test Node that creates a Channel with a less secret label and succeeds. #[test] fn create_channel_more_secret_label_ok() { @@ -171,6 +207,7 @@ fn create_channel_more_secret_label_ok() { }; run_node_body( &initial_label, + &NodePrivilege::default(), Box::new(move |runtime| { let result = runtime.channel_create(&more_secret_label); assert_eq!(true, result.is_ok()); @@ -186,6 +223,7 @@ fn create_node_same_label_ok() { let label_clone = label.clone(); run_node_body( &label, + &NodePrivilege::default(), Box::new(move |runtime| { let (_write_handle, read_handle) = runtime.channel_create(&label_clone)?; let result = runtime.node_create("log", "unused", &label_clone, read_handle); @@ -202,6 +240,7 @@ fn create_node_invalid_configuration_err() { let label_clone = label.clone(); run_node_body( &label, + &NodePrivilege::default(), Box::new(move |runtime| { let (_write_handle, read_handle) = runtime.channel_create(&label_clone)?; let result = runtime.node_create( @@ -236,6 +275,7 @@ fn create_node_less_secret_label_err() { let initial_label_clone = initial_label.clone(); run_node_body( &initial_label, + &NodePrivilege::default(), Box::new(move |runtime| { let (_write_handle, read_handle) = runtime.channel_create(&initial_label_clone)?; let result = runtime.node_create("log", "unused", &less_secret_label, read_handle); @@ -264,6 +304,7 @@ fn create_node_more_secret_label_ok() { let initial_label_clone = initial_label.clone(); run_node_body( &initial_label, + &NodePrivilege::default(), Box::new(move |runtime| { let (_write_handle, read_handle) = runtime.channel_create(&initial_label_clone)?; let result = runtime.node_create("log", "unused", &more_secret_label, read_handle); @@ -279,6 +320,7 @@ fn wait_on_channels_immediately_returns_if_any_channel_is_orphaned() { let label_clone = label.clone(); run_node_body( &label, + &NodePrivilege::default(), Box::new(move |runtime| { let (write_handle_0, read_handle_0) = runtime.channel_create(&label_clone)?; let (_write_handle_1, read_handle_1) = runtime.channel_create(&label_clone)?; @@ -306,6 +348,7 @@ fn wait_on_channels_blocks_if_all_channels_have_status_not_ready() { let label_clone = label.clone(); run_node_body( &label, + &NodePrivilege::default(), Box::new(move |runtime| { let (write_handle, read_handle) = runtime.channel_create(&label_clone)?; @@ -335,6 +378,7 @@ fn wait_on_channels_immediately_returns_if_any_channel_is_invalid() { let label_clone = label.clone(); run_node_body( &label, + &NodePrivilege::default(), Box::new(move |runtime| { let (write_handle, _read_handle) = runtime.channel_create(&label_clone)?; let (_write_handle, read_handle) = runtime.channel_create(&label_clone)?; @@ -357,6 +401,7 @@ fn wait_on_channels_immediately_returns_if_the_input_list_is_empty() { let label = test_label(); run_node_body( &label, + &NodePrivilege::default(), Box::new(|runtime| { let result = runtime.wait_on_channels(&[]); assert_eq!(Ok(Vec::::new()), result);