diff --git a/Cargo.lock b/Cargo.lock index 619689a1b57..a49055f19c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1388,6 +1388,7 @@ dependencies = [ "prost-types 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)", "tonic 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "wasmi 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2491,6 +2492,17 @@ dependencies = [ "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "sha2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "signal-hook" version = "0.1.13" @@ -3661,6 +3673,7 @@ dependencies = [ "checksum serial_test 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fef5f7c7434b2f2c598adc6f9494648a1e41274a75c0ba4056f680ae0c117fd6" "checksum serial_test_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d08338d8024b227c62bd68a12c7c9883f5c66780abaef15c550dc56f46ee6515" "checksum sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +"checksum sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "27044adfd2e1f077f649f59deb9490d3941d674002f7d062870a60ebe9bd47a0" "checksum signal-hook 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "10b9f3a1686a29f53cfd91ee5e3db3c12313ec02d33765f02c1a9645a1811e2c" "checksum signal-hook-registry 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41" "checksum simple_asn1 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b25ecba7165254f0c97d6c22a64b1122a03634b18d20a34daf21e18f892e618" diff --git a/docs/concepts.md b/docs/concepts.md index fd9d7c595ca..2224fd06313 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -230,12 +230,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/Cargo.toml b/oak/server/rust/oak_runtime/Cargo.toml index 4cc2c74426d..7bdeefcf1f2 100644 --- a/oak/server/rust/oak_runtime/Cargo.toml +++ b/oak/server/rust/oak_runtime/Cargo.toml @@ -33,6 +33,7 @@ prost = "*" prost-types = "*" rand = "*" regex = { version = "1", optional = true } +sha2 = "*" tokio = { version = "*", features = ["io-driver", "rt-core", "macros"] } # Using an old version that is supported by `cargo-raze`: # https://github.com/google/cargo-raze/issues/41#issuecomment-592274128 diff --git a/oak/server/rust/oak_runtime/src/runtime/mod.rs b/oak/server/rust/oak_runtime/src/runtime/mod.rs index d640cf04ca5..fb5372dd451 100644 --- a/oak/server/rust/oak_runtime/src/runtime/mod.rs +++ b/oak/server/rust/oak_runtime/src/runtime/mod.rs @@ -23,10 +23,13 @@ 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 oak_abi::{ + label::{Label, Tag}, + ChannelReadStatus, OakStatus, +}; use rand::RngCore; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, fmt::Write, string::String, sync::{Arc, Mutex, RwLock}, @@ -65,6 +68,14 @@ struct NodeInfo { /// See https://github.com/project-oak/oak/blob/master/docs/concepts.md#labels label: Label, + /// Tags that may be downgraded by the Node; either: + /// + /// - tags that may be declassified (removed from the secrecy component of a label) + /// - tags that may be endorsed (added to the integrity component of a label) + /// + /// See https://github.com/project-oak/oak/blob/master/docs/concepts.md#downgrades + can_downgrade_tags: HashSet, + /// Map of ABI handles to channels. abi_handles: HashMap, @@ -295,6 +306,7 @@ impl RuntimeProxy { proxy.node_id, "implicit.initial", &Label::public_trusted(), + &[], ); proxy } @@ -679,6 +691,34 @@ impl Runtime { node_info.label.clone() } + fn get_node_downgraded_label(&self, node_id: NodeId) -> Label { + let node_label = self.get_node_label(node_id); + let node_can_downgrade_tags = self.get_node_can_downgrade_tags(node_id); + Label { + secrecy_tags: node_label + .secrecy_tags + .iter() + .filter(|t| !node_can_downgrade_tags.contains(t)) + .cloned() + .collect(), + integrity_tags: node_label + .integrity_tags + .iter() + .chain(node_can_downgrade_tags.iter()) + .cloned() + .collect(), + } + } + + fn get_node_can_downgrade_tags(&self, node_id: NodeId) -> HashSet { + 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.can_downgrade_tags.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. @@ -693,62 +733,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) } } @@ -761,14 +805,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); @@ -1144,18 +1181,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 @@ -1170,7 +1199,7 @@ 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, &[]); let initial_handle = new_node_proxy .runtime .new_abi_handle(new_node_proxy.node_id, reader.clone()); @@ -1232,12 +1261,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, + can_downgrade_tags: &[Tag], + ) { self.add_node_info( node_id, NodeInfo { name: node_name.to_string(), label: label.clone(), + can_downgrade_tags: can_downgrade_tags.iter().cloned().collect(), 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 065dad82274..3ddef4b145d 100644 --- a/oak/server/rust/oak_runtime/src/runtime/tests.rs +++ b/oak/server/rust/oak_runtime/src/runtime/tests.rs @@ -20,11 +20,11 @@ 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, can_downgrade_tags: &[Tag], node_body: Box) { 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(), }; @@ -44,9 +44,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, + can_downgrade_tags, + ); let node_instance = TestNode { node_body }; let result = proxy.runtime.node_start_instance( @@ -77,6 +80,7 @@ fn panic_check() { let label = test_label(); run_node_body( &label, + &[], Box::new(|_runtime| { panic!("testing that panic works"); }), @@ -90,6 +94,7 @@ fn create_channel_same_label_ok() { let label_clone = label.clone(); run_node_body( &label, + &[], Box::new(move |runtime| { // Attempt to perform an operation that requires the [`Runtime`] to have created an // appropriate [`NodeInfo`] instance. @@ -107,18 +112,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, + &[], Box::new(move |runtime| { let result = runtime.channel_create(&less_secret_label); assert_eq!(Err(OakStatus::ErrPermissionDenied), result); @@ -127,6 +133,34 @@ fn create_channel_less_secret_label_err() { ); } +/// Create a test Node that creates a Channel with a less secret label and fails. +/// +/// If this succeeded, it would be a violation of information flow control, since the original +/// secret Node would be able to spawn "less secret / public" Channels and use their side effects as +/// a covert channel to exfiltrate secret data. +#[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, + &[tag_0], + 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() { @@ -145,6 +179,7 @@ fn create_channel_more_secret_label_ok() { }; run_node_body( &initial_label, + &[], Box::new(move |runtime| { let result = runtime.channel_create(&more_secret_label); assert_eq!(true, result.is_ok()); @@ -160,6 +195,7 @@ fn create_node_same_label_ok() { let label_clone = label.clone(); run_node_body( &label, + &[], 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); @@ -176,6 +212,7 @@ fn create_node_invalid_configuration_err() { let label_clone = label.clone(); run_node_body( &label, + &[], Box::new(move |runtime| { let (_write_handle, read_handle) = runtime.channel_create(&label_clone)?; let result = runtime.node_create( @@ -210,6 +247,7 @@ fn create_node_less_secret_label_err() { let initial_label_clone = initial_label.clone(); run_node_body( &initial_label, + &[], 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); @@ -238,6 +276,7 @@ fn create_node_more_secret_label_ok() { let initial_label_clone = initial_label.clone(); run_node_body( &initial_label, + &[], 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); @@ -253,6 +292,7 @@ fn wait_on_channels_immediately_returns_if_any_channel_is_orphaned() { let label_clone = label.clone(); run_node_body( &label, + &[], 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)?; @@ -280,6 +320,7 @@ fn wait_on_channels_blocks_if_all_channels_have_status_not_ready() { let label_clone = label.clone(); run_node_body( &label, + &[], Box::new(move |runtime| { let (write_handle, read_handle) = runtime.channel_create(&label_clone)?; @@ -309,6 +350,7 @@ fn wait_on_channels_immediately_returns_if_any_channel_is_invalid() { let label_clone = label.clone(); run_node_body( &label, + &[], Box::new(move |runtime| { let (write_handle, _read_handle) = runtime.channel_create(&label_clone)?; let (_write_handle, read_handle) = runtime.channel_create(&label_clone)?; @@ -331,6 +373,7 @@ fn wait_on_channels_immediately_returns_if_the_input_list_is_empty() { let label = test_label(); run_node_body( &label, + &[], Box::new(|runtime| { let result = runtime.wait_on_channels(&[]); assert_eq!(Ok(Vec::::new()), result);