Skip to content

Commit

Permalink
define and implement Secret Store ABI
Browse files Browse the repository at this point in the history
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"
```
  • Loading branch information
joeshaw committed Jan 18, 2023
1 parent c1fafea commit 080f558
Show file tree
Hide file tree
Showing 18 changed files with 706 additions and 5 deletions.
2 changes: 2 additions & 0 deletions cli/src/main.rs
Expand Up @@ -45,13 +45,15 @@ 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
.with_backends(backends.clone())
.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() {
Expand Down
9 changes: 8 additions & 1 deletion cli/tests/integration/common.rs
Expand Up @@ -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,
};

Expand Down Expand Up @@ -52,6 +54,7 @@ pub struct Test {
dictionaries: Dictionaries,
geolocation: Geolocation,
object_store: ObjectStore,
secret_stores: SecretStores,
hosts: Vec<HostSpec>,
log_stdout: bool,
log_stderr: bool,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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
})
}
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions cli/tests/integration/main.rs
Expand Up @@ -11,6 +11,7 @@ mod memory;
mod object_store;
mod request;
mod response;
mod secret_store;
mod sending_response;
mod sleep;
mod upstream;
Expand Down
216 changes: 216 additions & 0 deletions 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 <jbryson@fastly.com>", "Rose McDowall <rmcdowall@fastly.com>"]
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<FastlyConfig, FastlyConfigError> {
let toml = format!(
r#"
name = "secret-store"
description = "secret store test"
authors = ["Jill Bryson <jbryson@fastly.com>", "Rose McDowall <rmcdowall@fastly.com>"]
language = "rust"
[local_server]
{}
"#,
toml_fragment
);

println!("TOML: {}", toml);
toml.parse::<FastlyConfig>()
}

#[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(())
}
21 changes: 21 additions & 0 deletions lib/compute-at-edge-abi/compute-at-edge.witx
Expand Up @@ -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.
;;;
Expand Down
5 changes: 4 additions & 1 deletion lib/compute-at-edge-abi/typenames.witx
Expand Up @@ -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`.
;;;
Expand All @@ -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:
Expand Down

0 comments on commit 080f558

Please sign in to comment.