From 17fdd6e030a0433a22be29577342678561361a3d Mon Sep 17 00:00:00 2001 From: Joe Shaw Date: Thu, 29 Sep 2022 16:19:50 -0400 Subject: [PATCH] define and implement Secret Store ABI This introduces types and hostcalls for the Secret Store, and implements them in Viceroy, along with configuration to instantiate them. In Compute@Edge, a Secret Store is an encrypted, read-only key-value store for sensitive data. In Viceroy, however, it is a simple unencrypted in-memory map defined in the `fastly.toml` file in a manner similar to Object Stores. At a high level, a Wasm application using the APIs would: 1. Open a secret store by name 2. Get a secret from the store by name 3. Decrypt the secret by calling its `plaintext` method. In Viceroy, Secret Stores are configured in the same way Object Stores are: ```toml [local_server] [local_server.secret_store] store_one = [{key = "first", data = "This is some secret data"}, {key = "second", path = "/path/to/secret.json"}] [[local_server.secret_store.store_two]] key = "first" data = "This is also some secret data" [[local_server.secret_store.store_two]] key = "second" path = "/path/to/other/secret.json" ``` --- cli/src/main.rs | 2 + cli/tests/integration/common.rs | 9 +- cli/tests/integration/main.rs | 1 + cli/tests/integration/secret_store.rs | 216 +++++++++++++++++++ lib/compute-at-edge-abi/compute-at-edge.witx | 21 ++ lib/compute-at-edge-abi/typenames.witx | 5 +- lib/src/config.rs | 21 +- lib/src/config/secret_store.rs | 114 ++++++++++ lib/src/error.rs | 54 +++++ lib/src/execute.rs | 13 ++ lib/src/lib.rs | 1 + lib/src/linking.rs | 1 + lib/src/secret_store.rs | 60 ++++++ lib/src/session.rs | 50 ++++- lib/src/wiggle_abi.rs | 2 + lib/src/wiggle_abi/entity.rs | 4 +- lib/src/wiggle_abi/secret_store_impl.rs | 114 ++++++++++ test-fixtures/src/bin/secret-store.rs | 23 ++ 18 files changed, 706 insertions(+), 5 deletions(-) create mode 100644 cli/tests/integration/secret_store.rs create mode 100644 lib/src/config/secret_store.rs create mode 100644 lib/src/secret_store.rs create mode 100644 lib/src/wiggle_abi/secret_store_impl.rs create mode 100644 test-fixtures/src/bin/secret-store.rs diff --git a/cli/src/main.rs b/cli/src/main.rs index bfd0dca8..8b6f1a10 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -45,6 +45,7 @@ pub async fn serve(opts: Opts) -> Result<(), Error> { let geolocation = config.geolocation(); let dictionaries = config.dictionaries(); let object_store = config.object_store(); + let secret_stores = config.secret_stores(); let backend_names = itertools::join(backends.keys(), ", "); ctx = ctx @@ -52,6 +53,7 @@ pub async fn serve(opts: Opts) -> Result<(), Error> { .with_geolocation(geolocation.clone()) .with_dictionaries(dictionaries.clone()) .with_object_store(object_store.clone()) + .with_secret_stores(secret_stores.clone()) .with_config_path(config_path.into()); if backend_names.is_empty() { diff --git a/cli/tests/integration/common.rs b/cli/tests/integration/common.rs index 7651b5d1..3179ea49 100644 --- a/cli/tests/integration/common.rs +++ b/cli/tests/integration/common.rs @@ -11,7 +11,9 @@ use tokio::sync::Mutex; use tracing_subscriber::filter::EnvFilter; use viceroy_lib::{ body::Body, - config::{Backend, Backends, Dictionaries, FastlyConfig, Geolocation, ObjectStore}, + config::{ + Backend, Backends, Dictionaries, FastlyConfig, Geolocation, ObjectStore, SecretStores, + }, ExecuteCtx, ProfilingStrategy, ViceroyService, }; @@ -52,6 +54,7 @@ pub struct Test { dictionaries: Dictionaries, geolocation: Geolocation, object_store: ObjectStore, + secret_stores: SecretStores, hosts: Vec, log_stdout: bool, log_stderr: bool, @@ -70,6 +73,7 @@ impl Test { dictionaries: Dictionaries::new(), geolocation: Geolocation::new(), object_store: ObjectStore::new(), + secret_stores: SecretStores::new(), hosts: Vec::new(), log_stdout: false, log_stderr: false, @@ -88,6 +92,7 @@ impl Test { dictionaries: Dictionaries::new(), geolocation: Geolocation::new(), object_store: ObjectStore::new(), + secret_stores: SecretStores::new(), hosts: Vec::new(), log_stdout: false, log_stderr: false, @@ -103,6 +108,7 @@ impl Test { dictionaries: config.dictionaries().to_owned(), geolocation: config.geolocation().to_owned(), object_store: config.object_store().to_owned(), + secret_stores: config.secret_stores().to_owned(), ..self }) } @@ -206,6 +212,7 @@ impl Test { .with_dictionaries(self.dictionaries.clone()) .with_geolocation(self.geolocation.clone()) .with_object_store(self.object_store.clone()) + .with_secret_stores(self.secret_stores.clone()) .with_log_stderr(self.log_stderr) .with_log_stdout(self.log_stdout); let addr: SocketAddr = "127.0.0.1:17878".parse().unwrap(); diff --git a/cli/tests/integration/main.rs b/cli/tests/integration/main.rs index 60e9b2c2..64ddbe9a 100644 --- a/cli/tests/integration/main.rs +++ b/cli/tests/integration/main.rs @@ -11,6 +11,7 @@ mod memory; mod object_store; mod request; mod response; +mod secret_store; mod sending_response; mod sleep; mod upstream; diff --git a/cli/tests/integration/secret_store.rs b/cli/tests/integration/secret_store.rs new file mode 100644 index 00000000..c2ccfc2b --- /dev/null +++ b/cli/tests/integration/secret_store.rs @@ -0,0 +1,216 @@ +use crate::common::{Test, TestResult}; +use hyper::{body::to_bytes, StatusCode}; +use viceroy_lib::config::FastlyConfig; +use viceroy_lib::error::{FastlyConfigError, SecretStoreConfigError}; + +#[tokio::test(flavor = "multi_thread")] +async fn secret_store_works() -> TestResult { + const FASTLY_TOML: &str = r#" + name = "secret-store" + description = "secret store test" + authors = ["Jill Bryson ", "Rose McDowall "] + language = "rust" + [local_server] + secret_store.store_one = [{key = "first", data = "This is some data"},{key = "second", path = "../test-fixtures/data/object-store.txt"}] + "#; + + let resp = Test::using_fixture("secret-store.wasm") + .using_fastly_toml(FASTLY_TOML)? + .against_empty() + .await; + + assert_eq!(resp.status(), StatusCode::OK); + assert!(to_bytes(resp.into_body()) + .await + .expect("can read body") + .to_vec() + .is_empty()); + + Ok(()) +} + +fn bad_config_test(toml_fragment: &str) -> Result { + let toml = format!( + r#" + name = "secret-store" + description = "secret store test" + authors = ["Jill Bryson ", "Rose McDowall "] + language = "rust" + [local_server] + {} + "#, + toml_fragment + ); + + println!("TOML: {}", toml); + toml.parse::() +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_store_not_array() -> TestResult { + const TOML_FRAGMENT: &str = "secret_store.store_one = 1"; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidSecretStoreDefinition { + err: SecretStoreConfigError::NotAnArray, + .. + }) => (), + Err(_) => panic!("Expected a FastlyConfigError::InvalidSecretStoreDefinition with SecretStoreConfigError::NotAnArray"), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_store_not_table() -> TestResult { + const TOML_FRAGMENT: &str = "secret_store.store_one = [1]"; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidSecretStoreDefinition { + err: SecretStoreConfigError::NotATable, + .. + }) => (), + Err(_) => panic!("Expected a FastlyConfigError::InvalidSecretStoreDefinition with SecretStoreConfigError::NotATable"), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_no_key() -> TestResult { + const TOML_FRAGMENT: &str = r#"secret_store.store_one = [{data = "This is some data"}]"#; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidSecretStoreDefinition { + err: SecretStoreConfigError::NoKey, + .. + }) => (), + Err(_) => panic!("Expected a FastlyConfigError::InvalidSecretStoreDefinition with SecretStoreConfigError::NoKey"), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_key_not_string() -> TestResult { + const TOML_FRAGMENT: &str = + r#"secret_store.store_one = [{key = 1, data = "This is some data"}]"#; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidSecretStoreDefinition { + err: SecretStoreConfigError::KeyNotAString, + .. + }) => (), + Err(_) => panic!("Expected a FastlyConfigError::InvalidSecretStoreDefinition with SecretStoreConfigError::KeyNotAString"), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_no_data_or_path() -> TestResult { + const TOML_FRAGMENT: &str = r#"secret_store.store_one = [{key = "first"}]"#; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidSecretStoreDefinition { + err: SecretStoreConfigError::NoPathOrData(_), + .. + }) => (), + Err(_) => panic!("Expected a FastlyConfigError::InvalidSecretStoreDefinition with SecretStoreConfigError::NoPathOrData"), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_both_data_and_path() -> TestResult { + const TOML_FRAGMENT: &str = r#"secret_store.store_one = [{key = "first", path = "file.txt", data = "This is some data"}]"#; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidSecretStoreDefinition { + err: SecretStoreConfigError::PathAndData(_), + .. + }) => (), + Err(_) => panic!("Expected a FastlyConfigError::InvalidSecretStoreDefinition with SecretStoreConfigError::PathAndData"), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_data_not_string() -> TestResult { + const TOML_FRAGMENT: &str = r#"secret_store.store_one = [{key = "first", data = 1}]"#; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidSecretStoreDefinition { + err: SecretStoreConfigError::DataNotAString(_), + .. + }) => (), + Err(_) => panic!("Expected a FastlyConfigError::InvalidSecretStoreDefinition with SecretStoreConfigError::DataNotAString"), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_path_not_string() -> TestResult { + const TOML_FRAGMENT: &str = r#"secret_store.store_one = [{key = "first", path = 1}]"#; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidSecretStoreDefinition { + err: SecretStoreConfigError::PathNotAString(_), + .. + }) => (), + Err(_) => panic!("Expected a FastlyConfigError::InvalidSecretStoreDefinition with SecretStoreConfigError::PathNotAString"), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_path_nonexistent() -> TestResult { + const TOML_FRAGMENT: &str = + r#"secret_store.store_one = [{key = "first", path = "nonexistent.txt"}]"#; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidSecretStoreDefinition { + err: SecretStoreConfigError::IoError(_), + .. + }) => (), + Err(_) => panic!("Expected a FastlyConfigError::InvalidSecretStoreDefinition with SecretStoreConfigError::IoError"), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_invalid_store_name() -> TestResult { + const TOML_FRAGMENT: &str = + r#"secret_store.store*one = [{key = "first", data = "This is some data"}]"#; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidFastlyToml(_)) => (), + Err(_) => panic!("Expected a FastlyConfigError::InvalidFastlyToml"), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_invalid_secret_name() -> TestResult { + const TOML_FRAGMENT: &str = + r#"secret_store.store_one = [{key = "first*", data = "This is some data"}]"#; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidSecretStoreDefinition { + err: SecretStoreConfigError::InvalidSecretName(_), + .. + }) => (), + Err(_) => panic!("Expected a FastlyConfigError::InvalidSecretStoreDefinition with SecretStoreConfigError::InvalidSecretName"), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_secret_name_too_long() -> TestResult { + const TOML_FRAGMENT: &str = r#"secret_store.store_one = [{key = "firstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirstfirst", data = "This is some data"}]"#; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidSecretStoreDefinition { + err: SecretStoreConfigError::InvalidSecretName(_), + .. + }) => (), + Err(_) => panic!("Expected a FastlyConfigError::InvalidSecretStoreDefinition with SecretStoreConfigError::InvalidSecretName"), + _ => panic!("Expected an error"), + } + Ok(()) +} diff --git a/lib/compute-at-edge-abi/compute-at-edge.witx b/lib/compute-at-edge-abi/compute-at-edge.witx index 36319394..1c527190 100644 --- a/lib/compute-at-edge-abi/compute-at-edge.witx +++ b/lib/compute-at-edge-abi/compute-at-edge.witx @@ -508,6 +508,27 @@ ) ) +(module $fastly_secret_store + (@interface func (export "open") + (param $name string) + (result $err (expected $secret_store_handle (error $fastly_status))) + ) + + (@interface func (export "get") + (param $store $secret_store_handle) + (param $key string) + (result $err (expected $secret_handle (error $fastly_status))) + ) + + (@interface func (export "plaintext") + (param $secret $secret_handle) + (param $buf (@witx pointer (@witx char8))) + (param $buf_len (@witx usize)) + (param $nwritten_out (@witx pointer (@witx usize))) + (result $err (expected (error $fastly_status))) + ) +) + (module $fastly_async_io ;;; Blocks until one of the given objects is ready for I/O, or the optional timeout expires. ;;; diff --git a/lib/compute-at-edge-abi/typenames.witx b/lib/compute-at-edge-abi/typenames.witx index 4d1a9b42..5e55e65f 100644 --- a/lib/compute-at-edge-abi/typenames.witx +++ b/lib/compute-at-edge-abi/typenames.witx @@ -94,6 +94,10 @@ (typename $dictionary_handle (handle)) ;;; A handle to an Object Store. (typename $object_store_handle (handle)) +;;; A handle to a Secret Store. +(typename $secret_store_handle (handle)) +;;; A handle to an individual secret. +(typename $secret_handle (handle)) ;;; A handle to an object supporting generic async operations. ;;; Can be either a `body_handle` or a `pending_request_handle`. ;;; @@ -107,7 +111,6 @@ ;;; into, even before the origin itself consumes that data. (typename $async_item_handle (handle)) - ;;; A "multi-value" cursor. (typename $multi_value_cursor u32) ;;; -1 represents "finished", non-negative represents a $multi_value_cursor: diff --git a/lib/src/config.rs b/lib/src/config.rs index f8a053e1..09f921ce 100644 --- a/lib/src/config.rs +++ b/lib/src/config.rs @@ -2,7 +2,8 @@ use { self::{ - backends::BackendsConfig, dictionaries::DictionariesConfig, object_store::ObjectStoreConfig, + backends::BackendsConfig, dictionaries::DictionariesConfig, + object_store::ObjectStoreConfig, secret_store::SecretStoreConfig, }, crate::error::FastlyConfigError, serde_derive::Deserialize, @@ -42,6 +43,10 @@ mod object_store; pub use crate::object_store::ObjectStore; +/// Types and deserializers for secret store configuration settings. +mod secret_store; +pub use crate::secret_store::SecretStores; + /// Fastly-specific configuration information. /// /// This `struct` represents the fields and values in a Compute@Edge package's `fastly.toml`. @@ -94,6 +99,11 @@ impl FastlyConfig { &self.local_server.object_store.0 } + /// Get the secret store configuration. + pub fn secret_stores(&self) -> &SecretStores { + &self.local_server.secret_store.0 + } + /// Parse a `fastly.toml` file into a `FastlyConfig`. pub fn from_file(path: impl AsRef) -> Result { fs::read_to_string(path.as_ref()) @@ -175,6 +185,7 @@ pub struct LocalServerConfig { geolocation: Geolocation, dictionaries: DictionariesConfig, object_store: ObjectStoreConfig, + secret_store: SecretStoreConfig, } /// Enum of available (experimental) wasi modules @@ -193,6 +204,7 @@ struct RawLocalServerConfig { geolocation: Option, dictionaries: Option
, object_store: Option
, + secret_store: Option
, } impl TryInto for RawLocalServerConfig { @@ -203,6 +215,7 @@ impl TryInto for RawLocalServerConfig { geolocation, dictionaries, object_store, + secret_store, } = self; let backends = if let Some(backends) = backends { backends.try_into()? @@ -224,12 +237,18 @@ impl TryInto for RawLocalServerConfig { } else { ObjectStoreConfig::default() }; + let secret_store = if let Some(secret_store) = secret_store { + secret_store.try_into()? + } else { + SecretStoreConfig::default() + }; Ok(LocalServerConfig { backends, geolocation, dictionaries, object_store, + secret_store, }) } } diff --git a/lib/src/config/secret_store.rs b/lib/src/config/secret_store.rs new file mode 100644 index 00000000..14c0addb --- /dev/null +++ b/lib/src/config/secret_store.rs @@ -0,0 +1,114 @@ +use { + crate::{ + error::{FastlyConfigError, SecretStoreConfigError}, + secret_store::{SecretStore, SecretStores}, + }, + std::{convert::TryFrom, fs}, + toml::value::Table, +}; + +#[derive(Clone, Debug, Default)] +pub struct SecretStoreConfig(pub(crate) SecretStores); + +impl TryFrom
for SecretStoreConfig { + type Error = FastlyConfigError; + fn try_from(toml: Table) -> Result { + let mut stores = SecretStores::new(); + + for (store_name, items) in toml.iter() { + if !is_valid_name(store_name) { + return Err(FastlyConfigError::InvalidSecretStoreDefinition { + name: store_name.to_string(), + err: SecretStoreConfigError::InvalidSecretStoreName(store_name.to_string()), + }); + } + + let items = items.as_array().ok_or_else(|| { + FastlyConfigError::InvalidSecretStoreDefinition { + name: store_name.to_string(), + err: SecretStoreConfigError::NotAnArray, + } + })?; + + let mut secret_store = SecretStore::new(); + for item in items.iter() { + let item = item.as_table().ok_or_else(|| { + FastlyConfigError::InvalidSecretStoreDefinition { + name: store_name.to_string(), + err: SecretStoreConfigError::NotATable, + } + })?; + + let key = item + .get("key") + .ok_or_else(|| FastlyConfigError::InvalidSecretStoreDefinition { + name: store_name.to_string(), + err: SecretStoreConfigError::NoKey, + })? + .as_str() + .ok_or_else(|| FastlyConfigError::InvalidSecretStoreDefinition { + name: store_name.to_string(), + err: SecretStoreConfigError::KeyNotAString, + })?; + + if !is_valid_name(key) { + return Err(FastlyConfigError::InvalidSecretStoreDefinition { + name: store_name.to_string(), + err: SecretStoreConfigError::InvalidSecretName(key.to_string()), + }); + } + + let bytes = match (item.get("path"), item.get("data")) { + (None, None) => { + return Err(FastlyConfigError::InvalidSecretStoreDefinition { + name: store_name.to_string(), + err: SecretStoreConfigError::NoPathOrData(key.to_string()), + }) + } + (Some(_), Some(_)) => { + return Err(FastlyConfigError::InvalidSecretStoreDefinition { + name: store_name.to_string(), + err: SecretStoreConfigError::PathAndData(key.to_string()), + }) + } + (Some(path), None) => { + let path = path.as_str().ok_or_else(|| { + FastlyConfigError::InvalidSecretStoreDefinition { + name: store_name.to_string(), + err: SecretStoreConfigError::PathNotAString(key.to_string()), + } + })?; + fs::read(&path) + .map_err(|e| FastlyConfigError::InvalidSecretStoreDefinition { + name: store_name.to_string(), + err: SecretStoreConfigError::IoError(e), + })? + .into() + } + (None, Some(data)) => data + .as_str() + .ok_or_else(|| FastlyConfigError::InvalidSecretStoreDefinition { + name: store_name.to_string(), + err: SecretStoreConfigError::DataNotAString(key.to_string()), + })? + .to_owned() + .into(), + }; + secret_store.add_secret(key.to_string(), bytes); + } + stores.add_store(store_name.clone(), secret_store); + } + Ok(SecretStoreConfig(stores)) + } +} + +/// Human-readable names for Secret Stores and Secrets "must contain +/// only letters, numbers, dashes (-), underscores (_), and periods (.)" +/// They also have a maximum length of 255 bytes. +fn is_valid_name(name: &str) -> bool { + !name.is_empty() + && name.len() <= 255 + && name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.') +} diff --git a/lib/src/error.rs b/lib/src/error.rs index 0542e9f9..df4fa5fb 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -92,6 +92,9 @@ pub enum Error { #[error(transparent)] ObjectStoreError(#[from] crate::object_store::ObjectStoreError), + #[error(transparent)] + SecretStoreError(#[from] crate::wiggle_abi::SecretStoreError), + #[error{"Expected UTF-8"}] Utf8Expected(#[from] std::str::Utf8Error), @@ -148,6 +151,7 @@ impl Error { Error::DictionaryError(e) => e.to_fastly_status(), Error::GeolocationError(e) => e.to_fastly_status(), Error::ObjectStoreError(e) => e.into(), + Error::SecretStoreError(e) => e.into(), // All other hostcall errors map to a generic `ERROR` value. Error::AbiVersionMismatch | Error::BackendUrl(_) @@ -226,9 +230,19 @@ pub enum HandleError { /// A dictionary handle was not valid. #[error("Invalid dictionary handle: {0}")] InvalidDictionaryHandle(crate::wiggle_abi::types::DictionaryHandle), + /// An object-store handle was not valid. #[error("Invalid object-store handle: {0}")] InvalidObjectStoreHandle(crate::wiggle_abi::types::ObjectStoreHandle), + + /// A secret store handle was not valid. + #[error("Invalid secret store handle: {0}")] + InvalidSecretStoreHandle(crate::wiggle_abi::types::SecretStoreHandle), + + /// A secret handle was not valid. + #[error("Invalid secret handle: {0}")] + InvalidSecretHandle(crate::wiggle_abi::types::SecretHandle), + /// An async item handle was not valid. #[error("Invalid async item handle: {0}")] InvalidAsyncItemHandle(crate::wiggle_abi::types::AsyncItemHandle), @@ -303,6 +317,13 @@ pub enum FastlyConfigError { err: ObjectStoreConfigError, }, + #[error("invalid configuration for '{name}': {err}")] + InvalidSecretStoreDefinition { + name: String, + #[source] + err: SecretStoreConfigError, + }, + /// An error that occurred while deserializing the file. /// /// This represents errors caused by syntactically invalid TOML data, missing fields, etc. @@ -515,6 +536,39 @@ pub enum ObjectStoreConfigError { KeyValidationError(#[from] crate::object_store::KeyValidationError), } +/// Errors that may occur while validating secret store configurations. +#[derive(Debug, thiserror::Error)] +pub enum SecretStoreConfigError { + /// An I/O error that occured while reading the file. + #[error(transparent)] + IoError(std::io::Error), + + #[error("The `path` and `data` keys for the object `{0}` are set. Only one can be used.")] + PathAndData(String), + #[error("The `path` or `data` key for the object `{0}` is not set. One must be used.")] + NoPathOrData(String), + #[error("The `data` value for the object `{0}` is not a string.")] + DataNotAString(String), + #[error("The `path` value for the object `{0}` is not a string.")] + PathNotAString(String), + + #[error("The `key` key for an object is not set. It must be used.")] + NoKey, + #[error("The `key` value for an object is not a string.")] + KeyNotAString, + + #[error("There is no array of objects for the given store.")] + NotAnArray, + #[error("There is an object in the given store that is not a table of keys.")] + NotATable, + + #[error("Invalid secret store name: {0}")] + InvalidSecretStoreName(String), + + #[error("Invalid secret name: {0}")] + InvalidSecretName(String), +} + /// Errors related to the downstream request. #[derive(Debug, thiserror::Error)] pub enum DownstreamRequestError { diff --git a/lib/src/execute.rs b/lib/src/execute.rs index eb9b3b5e..fe7b00ec 100644 --- a/lib/src/execute.rs +++ b/lib/src/execute.rs @@ -8,6 +8,7 @@ use { error::ExecutionError, linking::{create_store, dummy_store, link_host_functions, WasmCtx}, object_store::ObjectStore, + secret_store::SecretStores, session::Session, upstream::TlsConfig, Error, @@ -56,6 +57,8 @@ pub struct ExecuteCtx { next_req_id: Arc, /// The ObjectStore associated with this instance of Viceroy object_store: Arc, + /// The secret stores for this execution. + secret_stores: Arc, } impl ExecuteCtx { @@ -86,6 +89,7 @@ impl ExecuteCtx { log_stderr: false, next_req_id: Arc::new(AtomicU64::new(0)), object_store: Arc::new(ObjectStore::new()), + secret_stores: Arc::new(SecretStores::new()), }) } @@ -141,6 +145,14 @@ impl ExecuteCtx { } } + /// Set the secret stores for this execution context. + pub fn with_secret_stores(self, secret_stores: SecretStores) -> Self { + Self { + secret_stores: Arc::new(secret_stores), + ..self + } + } + /// Set the path to the config for this execution context. pub fn with_config_path(self, config_path: PathBuf) -> Self { Self { @@ -284,6 +296,7 @@ impl ExecuteCtx { self.dictionaries.clone(), self.config_path.clone(), self.object_store.clone(), + self.secret_stores.clone(), ); // We currently have to postpone linking and instantiation to the guest task // due to wasmtime limitations, in particular the fact that `Instance` is not `Send`. diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 9abcf1c7..a2383aff 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -25,6 +25,7 @@ mod execute; mod headers; mod linking; mod object_store; +mod secret_store; mod service; mod streaming_body; mod upstream; diff --git a/lib/src/linking.rs b/lib/src/linking.rs index 495d27eb..1b3d0e1f 100644 --- a/lib/src/linking.rs +++ b/lib/src/linking.rs @@ -121,6 +121,7 @@ pub fn link_host_functions( wiggle_abi::fastly_log::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_object_store::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_purge::add_to_linker(linker, WasmCtx::session)?; + wiggle_abi::fastly_secret_store::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_uap::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_async_io::add_to_linker(linker, WasmCtx::session)?; link_legacy_aliases(linker)?; diff --git a/lib/src/secret_store.rs b/lib/src/secret_store.rs new file mode 100644 index 00000000..1804241c --- /dev/null +++ b/lib/src/secret_store.rs @@ -0,0 +1,60 @@ +use {bytes::Bytes, std::collections::HashMap}; + +#[derive(Clone, Debug, Default)] +pub struct SecretStores { + stores: HashMap, +} + +impl SecretStores { + pub fn new() -> Self { + Self { + stores: HashMap::new(), + } + } + + pub fn get_store(&self, name: &str) -> Option<&SecretStore> { + self.stores.get(name) + } + + pub fn add_store(&mut self, name: String, store: SecretStore) { + self.stores.insert(name, store); + } +} + +#[derive(Clone, Debug, Default)] +pub struct SecretStore { + secrets: HashMap, +} + +impl SecretStore { + pub fn new() -> Self { + Self { + secrets: HashMap::new(), + } + } + + pub fn get_secret(&self, name: &str) -> Option<&Secret> { + self.secrets.get(name) + } + + pub fn add_secret(&mut self, name: String, secret: Bytes) { + self.secrets.insert(name, Secret { plaintext: secret }); + } +} + +#[derive(Clone, Debug, Default)] +pub struct Secret { + plaintext: Bytes, +} + +impl Secret { + pub fn plaintext(&self) -> &[u8] { + &self.plaintext + } +} + +#[derive(Clone, Debug, Default)] +pub struct SecretLookup { + pub store_name: String, + pub secret_name: String, +} diff --git a/lib/src/session.rs b/lib/src/session.rs index d8d923a6..00294e25 100644 --- a/lib/src/session.rs +++ b/lib/src/session.rs @@ -13,11 +13,13 @@ use { error::{Error, HandleError}, logging::LogEndpoint, object_store::{ObjectKey, ObjectStore, ObjectStoreError, ObjectStoreKey}, + secret_store::{SecretLookup, SecretStores}, streaming_body::StreamingBody, upstream::{PendingRequest, SelectTarget, TlsConfig}, wiggle_abi::types::{ self, BodyHandle, ContentEncodings, DictionaryHandle, EndpointHandle, - ObjectStoreHandle, PendingRequestHandle, RequestHandle, ResponseHandle, + ObjectStoreHandle, PendingRequestHandle, RequestHandle, ResponseHandle, SecretHandle, + SecretStoreHandle, }, }, cranelift_entity::{entity_impl, PrimaryMap}, @@ -97,6 +99,18 @@ pub struct Session { /// /// Populated prior to guest execution. object_store_by_name: PrimaryMap, + /// The secret stores configured for this execution. + /// + /// Populated prior to guest execution, and never modified. + secret_stores: Arc, + /// The secret stores configured for this execution. + /// + /// Populated prior to guest execution, and never modified. + secret_stores_by_name: PrimaryMap, + /// The secrets for this execution. + /// + /// Populated prior to guest execution, and never modified. + secrets_by_name: PrimaryMap, /// The path to the configuration file used for this invocation of Viceroy. /// /// Created prior to guest execution, and never modified. @@ -119,6 +133,7 @@ impl Session { dictionaries: Arc, config_path: Arc>, object_store: Arc, + secret_stores: Arc, ) -> Session { let (parts, body) = req.into_parts(); let downstream_req_original_headers = parts.headers.clone(); @@ -148,6 +163,9 @@ impl Session { dictionaries_by_name: PrimaryMap::new(), object_store, object_store_by_name: PrimaryMap::new(), + secret_stores, + secret_stores_by_name: PrimaryMap::new(), + secrets_by_name: PrimaryMap::new(), config_path, req_id, } @@ -172,6 +190,7 @@ impl Session { Arc::new(HashMap::new()), Arc::new(None), Arc::new(ObjectStore::new()), + Arc::new(SecretStores::new()), ) } @@ -638,6 +657,35 @@ impl Session { self.object_store.lookup(obj_store_key, obj_key) } + // ----- Secret Store API ----- + + pub fn secret_store_handle(&mut self, name: &str) -> Option { + self.secret_stores.get_store(name)?; + Some(self.secret_stores_by_name.push(name.to_string())) + } + + pub fn secret_store_name(&self, handle: SecretStoreHandle) -> Option { + self.secret_stores_by_name.get(handle).cloned() + } + + pub fn secret_handle(&mut self, store_name: &str, secret_name: &str) -> Option { + self.secret_stores + .get_store(store_name)? + .get_secret(secret_name)?; + Some(self.secrets_by_name.push(SecretLookup { + store_name: store_name.to_string(), + secret_name: secret_name.to_string(), + })) + } + + pub fn secret_lookup(&self, handle: SecretHandle) -> Option { + self.secrets_by_name.get(handle).cloned() + } + + pub fn secret_stores(&self) -> &Arc { + &self.secret_stores + } + // ----- Pending Requests API ----- /// Insert a [`PendingRequest`] into the session. diff --git a/lib/src/wiggle_abi.rs b/lib/src/wiggle_abi.rs index 64742dc7..e3c1e915 100644 --- a/lib/src/wiggle_abi.rs +++ b/lib/src/wiggle_abi.rs @@ -10,6 +10,7 @@ // documentation which includes the code generated by Wiggle, and then open it in your browser. pub use self::dictionary_impl::DictionaryError; +pub use self::secret_store_impl::SecretStoreError; pub use self::geo_impl::GeolocationError; @@ -56,6 +57,7 @@ mod log_impl; mod obj_store_impl; mod req_impl; mod resp_impl; +mod secret_store_impl; mod uap_impl; // Expand the `.witx` interface definition into a collection of modules. The `types` module will diff --git a/lib/src/wiggle_abi/entity.rs b/lib/src/wiggle_abi/entity.rs index 3efa55af..1641ade8 100644 --- a/lib/src/wiggle_abi/entity.rs +++ b/lib/src/wiggle_abi/entity.rs @@ -4,7 +4,7 @@ use super::types::{ AsyncItemHandle, BodyHandle, DictionaryHandle, EndpointHandle, ObjectStoreHandle, - PendingRequestHandle, RequestHandle, ResponseHandle, + PendingRequestHandle, RequestHandle, ResponseHandle, SecretHandle, SecretStoreHandle, }; /// Macro which implements a 32-bit entity reference for handles generated by Wiggle. @@ -46,4 +46,6 @@ wiggle_entity!(EndpointHandle); wiggle_entity!(PendingRequestHandle); wiggle_entity!(DictionaryHandle); wiggle_entity!(ObjectStoreHandle); +wiggle_entity!(SecretStoreHandle); +wiggle_entity!(SecretHandle); wiggle_entity!(AsyncItemHandle); diff --git a/lib/src/wiggle_abi/secret_store_impl.rs b/lib/src/wiggle_abi/secret_store_impl.rs new file mode 100644 index 00000000..f44bdbe1 --- /dev/null +++ b/lib/src/wiggle_abi/secret_store_impl.rs @@ -0,0 +1,114 @@ +use { + crate::{ + error::Error, + session::Session, + wiggle_abi::{ + fastly_secret_store::FastlySecretStore, + types::{FastlyStatus, SecretHandle, SecretStoreHandle}, + }, + }, + std::convert::TryFrom, + wiggle::GuestPtr, +}; + +#[derive(Debug, thiserror::Error)] +pub enum SecretStoreError { + /// A secret store with the given name was not found. + #[error("Unknown secret store: {0}")] + UnknownSecretStore(String), + + /// A secret with the given name was not found. + #[error("Unknown secret: {0}")] + UnknownSecret(String), + + /// An invalid secret store handle was provided. + #[error("Invalid secret store handle: {0}")] + InvalidSecretStoreHandle(SecretStoreHandle), + + /// An invalid secret handle was provided. + #[error("Invalid secret handle: {0}")] + InvalidSecretHandle(SecretHandle), +} + +impl From<&SecretStoreError> for FastlyStatus { + fn from(err: &SecretStoreError) -> Self { + use SecretStoreError::*; + match err { + UnknownSecretStore(_) => FastlyStatus::None, + UnknownSecret(_) => FastlyStatus::None, + InvalidSecretStoreHandle(_) => FastlyStatus::Badf, + InvalidSecretHandle(_) => FastlyStatus::Badf, + } + } +} + +#[wiggle::async_trait] +impl FastlySecretStore for Session { + fn open(&mut self, name: &GuestPtr) -> Result { + self.secret_store_handle(&name.as_str()?) + .ok_or(Error::SecretStoreError( + SecretStoreError::UnknownSecretStore(name.as_str()?.to_string()), + )) + } + + fn get( + &mut self, + secret_store_handle: SecretStoreHandle, + secret_name: &GuestPtr, + ) -> Result { + let store_name = + self.secret_store_name(secret_store_handle) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretStoreHandle(secret_store_handle), + ))?; + + self.secret_handle(store_name.as_str(), &secret_name.as_str()?) + .ok_or(Error::SecretStoreError(SecretStoreError::UnknownSecret( + secret_name.as_str()?.to_string(), + ))) + } + + fn plaintext( + &mut self, + secret_handle: SecretHandle, + plaintext_buf: &GuestPtr, + plaintext_max_len: u32, + nwritten_out: &GuestPtr, + ) -> Result<(), Error> { + let lookup = self + .secret_lookup(secret_handle) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretHandle(secret_handle), + ))?; + let plaintext = self + .secret_stores() + .get_store(lookup.store_name.as_str()) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretHandle(secret_handle), + ))? + .get_secret(lookup.secret_name.as_str()) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretHandle(secret_handle), + ))? + .plaintext(); + + if plaintext.len() > plaintext_max_len as usize { + // Write out the number of bytes necessary to fit the + // plaintext, so client implementations can adapt their + // buffer sizes. + nwritten_out.write(plaintext.len() as u32)?; + return Err(Error::BufferLengthError { + buf: "plaintext_buf", + len: "plaintext_max_len", + }); + } + let plaintext_len = u32::try_from(plaintext.len()) + .expect("smaller than plaintext_max_len means it must fit"); + + let mut plaintext_out = plaintext_buf.as_array(plaintext_len).as_slice_mut()?; + plaintext_out.copy_from_slice(&plaintext); + nwritten_out.write(plaintext_len)?; + + Ok(()) + } +} diff --git a/test-fixtures/src/bin/secret-store.rs b/test-fixtures/src/bin/secret-store.rs new file mode 100644 index 00000000..b2d6a0ce --- /dev/null +++ b/test-fixtures/src/bin/secret-store.rs @@ -0,0 +1,23 @@ +//! A guest program to test that secret store works properly. + +use fastly::SecretStore; + +fn main() { + // Check we can't get a store that does not exist + match SecretStore::open("nonexistent") { + Err(_) => {} + _ => panic!(), + } + + let store_one = SecretStore::open("store_one").unwrap(); + assert_eq!( + store_one.get("first").unwrap().plaintext(), + "This is some data" + ); + assert_eq!(store_one.get("second").unwrap().plaintext(), "More data"); + + match store_one.try_get("third").unwrap() { + None => {} + _ => panic!(), + } +}