diff --git a/docs/concepts.md b/docs/concepts.md index 332fabfce6f..c0e0cb18a9e 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -249,12 +249,34 @@ 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 privilege 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 may 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 privilege 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. + +When Node A attempts to create another Node B, it specifies the desired label +for B, which is checked by the Oak Runtime to be allowed by the "flows to" +relationship, but A does not have a way to influence the privilege of B; this is +always entirely determined by the Oak Runtime itself, which is trusted to assign +the appropriate privilege to all Nodes. + +#### 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 77130f21747..2b1135709aa 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}, @@ -103,6 +106,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, @@ -112,6 +118,18 @@ struct NodeInfo { node_stopper: 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!( @@ -341,6 +359,7 @@ impl RuntimeProxy { proxy.node_id, "implicit.initial", &Label::public_trusted(), + &NodePrivilege::default(), ); proxy } @@ -725,6 +744,42 @@ impl Runtime { node_info.label.clone() } + /// Returns the least restrictive (i.e. least secret, most trusted) label that this Node may + /// downgrade to. This takes 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. @@ -739,62 +794,67 @@ 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 all the [downgrade privilege](NodeInfo::privilege) the Node + /// possesses. + 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 all the [downgrade privilege](NodeInfo::privilege) the Node possesses. + 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) } } @@ -807,14 +867,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); @@ -1180,18 +1233,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 @@ -1206,7 +1251,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()); @@ -1275,12 +1325,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(), node_stopper: None, }, diff --git a/oak/server/rust/oak_runtime/src/runtime/tests.rs b/oak/server/rust/oak_runtime/src/runtime/tests.rs index 0f31cfe5e85..624ea6084dd 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(), }; @@ -62,9 +63,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"); @@ -108,6 +112,7 @@ fn panic_check() { let label = test_label(); run_node_body( &label, + &NodePrivilege::default(), Box::new(|_runtime| { panic!("testing that panic works"); }), @@ -121,6 +126,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. @@ -138,18 +144,80 @@ 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); + Ok(()) + }), + ); +} + +/// Create a test Node that creates a Channel with a less secret label and succeeds, because the +/// node is granted the ability to declassify the removed secrecy tag. +#[test] +fn create_channel_less_secret_label_declassification_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 other_tag = oak_abi::label::authorization_bearer_token_hmac_tag(&[3, 3, 3]); + 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` and another unrelated tag, and + // endorse another unrelated tag. + &NodePrivilege { + can_declassify_secrecy_tags: hashset! { tag_0, other_tag.clone() }, + can_endorse_integrity_tags: hashset! { other_tag }, + }, + 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 fails, because the node +/// is granted the ability to endorse (rather than declassify) the removed secrecy tag. +#[test] +fn create_channel_less_secret_label_no_privilege_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![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 endorse (rather than declassify) `tag_0`, which in this + // case is useless, so it should still fail. + &NodePrivilege { + can_declassify_secrecy_tags: hashset! {}, + can_endorse_integrity_tags: hashset! { tag_0 }, + }, Box::new(move |runtime| { let result = runtime.channel_create(&less_secret_label); assert_eq!(Err(OakStatus::ErrPermissionDenied), result); @@ -158,24 +226,24 @@ fn create_channel_less_secret_label_err() { ); } -/// Create a test Node that creates a Channel with a less secret label and succeeds. +/// Create a test Node that creates a Channel with a more secret label and succeeds. +/// +/// Data is always allowed to flow to more secret labels. #[test] fn create_channel_more_secret_label_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![oak_abi::label::authorization_bearer_token_hmac_tag(&[ - 1, 1, 1, - ])], + secrecy_tags: vec![tag_0.clone()], integrity_tags: vec![], }; let more_secret_label = Label { - secrecy_tags: vec![ - oak_abi::label::authorization_bearer_token_hmac_tag(&[1, 1, 1]), - oak_abi::label::authorization_bearer_token_hmac_tag(&[2, 2, 2]), - ], + secrecy_tags: vec![tag_0, tag_1], integrity_tags: vec![], }; 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()); @@ -191,6 +259,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); @@ -207,6 +276,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( @@ -228,10 +298,9 @@ fn create_node_invalid_configuration_err() { /// covert channel to exfiltrate secret data. #[test] fn create_node_less_secret_label_err() { + let tag_0 = oak_abi::label::authorization_bearer_token_hmac_tag(&[1, 1, 1]); let initial_label = Label { - secrecy_tags: vec![oak_abi::label::authorization_bearer_token_hmac_tag(&[ - 1, 1, 1, - ])], + secrecy_tags: vec![tag_0], integrity_tags: vec![], }; let less_secret_label = Label { @@ -241,6 +310,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); @@ -253,22 +323,20 @@ fn create_node_less_secret_label_err() { /// Create a test Node that creates a Node with a more secret label and succeeds. #[test] fn create_node_more_secret_label_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![oak_abi::label::authorization_bearer_token_hmac_tag(&[ - 1, 1, 1, - ])], + secrecy_tags: vec![tag_0.clone()], integrity_tags: vec![], }; let more_secret_label = Label { - secrecy_tags: vec![ - oak_abi::label::authorization_bearer_token_hmac_tag(&[1, 1, 1]), - oak_abi::label::authorization_bearer_token_hmac_tag(&[2, 2, 2]), - ], + secrecy_tags: vec![tag_0, tag_1], integrity_tags: vec![], }; 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); @@ -284,6 +352,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)?; @@ -311,6 +380,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)?; @@ -340,6 +410,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)?; @@ -362,6 +433,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);