diff --git a/Cargo.lock b/Cargo.lock index c2cc4a121fa..fc1a193a447 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4784,6 +4784,17 @@ dependencies = [ "trybuild", ] +[[package]] +name = "spacetimedb-auth" +version = "1.0.0-rc3" +dependencies = [ + "anyhow", + "serde", + "serde_with", + "spacetimedb-jsonwebtoken", + "spacetimedb-lib", +] + [[package]] name = "spacetimedb-bench" version = "1.0.0-rc3" @@ -4877,8 +4888,8 @@ dependencies = [ "serde_json", "serde_with", "slab", + "spacetimedb-auth", "spacetimedb-client-api-messages", - "spacetimedb-core", "spacetimedb-data-structures", "spacetimedb-fs-utils", "spacetimedb-jsonwebtoken", @@ -5059,6 +5070,7 @@ dependencies = [ "slab", "sled", "smallvec", + "spacetimedb-auth", "spacetimedb-client-api-messages", "spacetimedb-commitlog", "spacetimedb-data-structures", @@ -5087,7 +5099,6 @@ dependencies = [ "tokio-stream", "tokio-util", "toml 0.8.19", - "toml_edit", "tracing", "tracing-appender", "tracing-core", diff --git a/Cargo.toml b/Cargo.toml index 4a65fdd321f..70ba260a635 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "crates/auth", "crates/bench", "crates/bindings-sys", "crates/bindings", @@ -92,6 +93,7 @@ rust-version = "1.78.0" [workspace.dependencies] spacetimedb = { path = "crates/bindings", version = "1.0.0-rc3" } +spacetimedb-auth = { path = "crates/auth", version = "1.0.0-rc3" } spacetimedb-bindings-macro = { path = "crates/bindings-macro", version = "1.0.0-rc3" } spacetimedb-bindings-sys = { path = "crates/bindings-sys", version = "1.0.0-rc3" } spacetimedb-cli = { path = "crates/cli", version = "1.0.0-rc3" } @@ -181,6 +183,7 @@ itertools = "0.12" itoa = "1" jsonwebtoken = { package = "spacetimedb-jsonwebtoken", version = "9.3.0" } junction = "1" +jwks = { package = "spacetimedb-jwks", version = "0.1.3" } lazy_static = "1.4.0" log = "0.4.17" memchr = "2" diff --git a/crates/auth/Cargo.toml b/crates/auth/Cargo.toml new file mode 100644 index 00000000000..1885480f4bc --- /dev/null +++ b/crates/auth/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "spacetimedb-auth" +version.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +spacetimedb-lib.workspace = true + +anyhow.workspace = true +serde.workspace = true +serde_with.workspace = true +jsonwebtoken.workspace = true diff --git a/crates/auth/src/identity.rs b/crates/auth/src/identity.rs new file mode 100644 index 00000000000..0e8c9681e90 --- /dev/null +++ b/crates/auth/src/identity.rs @@ -0,0 +1,89 @@ +pub use jsonwebtoken::errors::Error as JwtError; +pub use jsonwebtoken::errors::ErrorKind as JwtErrorKind; +pub use jsonwebtoken::{DecodingKey, EncodingKey}; +use serde::{Deserialize, Serialize}; +use spacetimedb_lib::Identity; +use std::time::SystemTime; + +// These are the claims that can be attached to a request/connection. +#[serde_with::serde_as] +#[derive(Debug, Serialize, Deserialize)] +pub struct SpacetimeIdentityClaims { + #[serde(rename = "hex_identity")] + pub identity: Identity, + #[serde(rename = "sub")] + pub subject: String, + #[serde(rename = "iss")] + pub issuer: String, + #[serde(rename = "aud")] + pub audience: Vec, + + /// The unix timestamp the token was issued at + #[serde_as(as = "serde_with::TimestampSeconds")] + pub iat: SystemTime, + #[serde_as(as = "Option")] + pub exp: Option, +} + +// IncomingClaims are from the token we receive from the client. +// The signature should be verified already, but further validation is needed to have a SpacetimeIdentityClaims2. +#[serde_with::serde_as] +#[derive(Debug, Serialize, Deserialize)] +pub struct IncomingClaims { + #[serde(rename = "hex_identity")] + pub identity: Option, + #[serde(rename = "sub")] + pub subject: String, + #[serde(rename = "iss")] + pub issuer: String, + #[serde(rename = "aud", default)] + pub audience: Vec, + + /// The unix timestamp the token was issued at + #[serde_as(as = "serde_with::TimestampSeconds")] + pub iat: SystemTime, + #[serde_as(as = "Option")] + pub exp: Option, +} + +impl TryInto for IncomingClaims { + type Error = anyhow::Error; + + fn try_into(self) -> anyhow::Result { + // The issuer and subject must be less than 128 bytes. + if self.issuer.len() > 128 { + return Err(anyhow::anyhow!("Issuer too long: {:?}", self.issuer)); + } + if self.subject.len() > 128 { + return Err(anyhow::anyhow!("Subject too long: {:?}", self.subject)); + } + // The issuer and subject must be non-empty. + if self.issuer.is_empty() { + return Err(anyhow::anyhow!("Issuer empty")); + } + if self.subject.is_empty() { + return Err(anyhow::anyhow!("Subject empty")); + } + + let computed_identity = Identity::from_claims(&self.issuer, &self.subject); + // If an identity is provided, it must match the computed identity. + if let Some(token_identity) = self.identity { + if token_identity != computed_identity { + return Err(anyhow::anyhow!( + "Identity mismatch: token identity {:?} does not match computed identity {:?}", + token_identity, + computed_identity, + )); + } + } + + Ok(SpacetimeIdentityClaims { + identity: computed_identity, + subject: self.subject, + issuer: self.issuer, + audience: self.audience, + iat: self.iat, + exp: self.exp, + }) + } +} diff --git a/crates/auth/src/lib.rs b/crates/auth/src/lib.rs new file mode 100644 index 00000000000..db53a0c9064 --- /dev/null +++ b/crates/auth/src/lib.rs @@ -0,0 +1 @@ +pub mod identity; diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index a8bbf5e7ddc..0c263608bbc 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -18,9 +18,9 @@ bench = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +spacetimedb-auth.workspace = true spacetimedb-client-api-messages.workspace = true -spacetimedb-core.workspace = true -spacetimedb-data-structures.workspace = true +spacetimedb-data-structures = { workspace = true, features = ["serde"] } spacetimedb-fs-utils.workspace = true spacetimedb-lib.workspace = true spacetimedb-paths.workspace = true diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index cfd2fe36f8a..75e8b47a017 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -2,7 +2,6 @@ use crate::errors::CliError; use crate::util::{contains_protocol, host_or_url_to_host_and_protocol}; use anyhow::Context; use jsonwebtoken::DecodingKey; -use spacetimedb::config::{set_opt_value, set_table_opt_value}; use spacetimedb_fs_utils::atomic_write; use spacetimedb_paths::cli::CliTomlPath; use std::collections::HashMap; @@ -830,6 +829,53 @@ Update the server's fingerprint with: } } +/// Update the value of a key in a `TOML` document, preserving the formatting and comments of the original value. +/// +/// ie: +/// +/// ```toml;no_run +/// # Moving key = value to key = new_value +/// old = "value" # Comment +/// new = "new_value" # Comment +/// ``` +fn copy_value_with_decor(old_value: Option<&toml_edit::Item>, new_value: &str) -> toml_edit::Item { + match old_value { + Some(toml_edit::Item::Value(toml_edit::Value::String(old_value))) => { + // Creates a new `toml_edit::Value` with the same formatting as the old value. + let mut new = toml_edit::Value::String(toml_edit::Formatted::new(new_value.to_string())); + let decor = new.decor_mut(); + // Copy the comments and formatting from the old value. + *decor = old_value.decor().clone(); + new.into() + } + _ => new_value.into(), + } +} + +/// Set the value of a key in a `TOML` document, removing the key if the value is `None`. +/// +/// **NOTE**: This function will preserve the formatting and comments of the original value. +pub fn set_opt_value(doc: &mut toml_edit::DocumentMut, key: &str, value: Option<&str>) { + let old_value = doc.get(key); + if let Some(new) = value { + doc[key] = copy_value_with_decor(old_value, new); + } else { + doc.remove(key); + } +} + +/// Set the value of a key in a `TOML` table, removing the key if the value is `None`. +/// +/// **NOTE**: This function will preserve the formatting and comments of the original value. +pub fn set_table_opt_value(table: &mut toml_edit::Table, key: &str, value: Option<&str>) { + let old_value = table.get(key); + if let Some(new) = value { + table[key] = copy_value_with_decor(old_value, new); + } else { + table.remove(key); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/cli/src/subcommands/call.rs b/crates/cli/src/subcommands/call.rs index df8a58e62de..50f07f865a4 100644 --- a/crates/cli/src/subcommands/call.rs +++ b/crates/cli/src/subcommands/call.rs @@ -7,10 +7,9 @@ use anyhow::{bail, Context, Error}; use clap::{Arg, ArgMatches}; use itertools::Either; use serde_json::Value; -use spacetimedb::Identity; use spacetimedb_lib::de::serde::deserialize_from; use spacetimedb_lib::sats::{AlgebraicType, AlgebraicTypeRef, Typespace}; -use spacetimedb_lib::ProductTypeElement; +use spacetimedb_lib::{Identity, ProductTypeElement}; use std::fmt::Write; use std::iter; diff --git a/crates/cli/src/subcommands/generate/mod.rs b/crates/cli/src/subcommands/generate/mod.rs index e220b66b02c..2738d4d041c 100644 --- a/crates/cli/src/subcommands/generate/mod.rs +++ b/crates/cli/src/subcommands/generate/mod.rs @@ -8,7 +8,6 @@ use convert_case::{Case, Casing}; use core::mem; use duct::cmd; use itertools::Itertools; -use spacetimedb::host::wasmtime::{Mem, MemView, WasmPointee as _}; use spacetimedb_data_structures::map::HashSet; use spacetimedb_lib::de::serde::DeserializeWrapper; use spacetimedb_lib::sats::{AlgebraicType, AlgebraicTypeRef, Typespace}; @@ -404,13 +403,13 @@ fn extract_descriptions_from_module(module: wasmtime::Module) -> anyhow::Result< message_ptr: u32, message_len: u32| { let (mem, _) = WasmCtx::mem_env(&mut caller); - let slice = mem.deref_slice(message_ptr, message_len).unwrap(); + let slice = deref_slice(mem, message_ptr, message_len).unwrap(); println!("from wasm: {}", String::from_utf8_lossy(slice)); }, )?; linker.func_wrap(module_name, "bytes_sink_write", WasmCtx::bytes_sink_write)?; let instance = linker.instantiate(&mut store, &module)?; - let memory = Mem::extract(&instance, &mut store)?; + let memory = instance.get_memory(&mut store, "memory").context("no memory export")?; store.data_mut().mem = Some(memory); let mut preinits = instance @@ -435,19 +434,26 @@ fn extract_descriptions_from_module(module: wasmtime::Module) -> anyhow::Result< } struct WasmCtx { - mem: Option, + mem: Option, sink: Vec, } +fn deref_slice(mem: &[u8], offset: u32, len: u32) -> anyhow::Result<&[u8]> { + anyhow::ensure!(offset != 0, "ptr is null"); + mem.get(offset as usize..) + .and_then(|s| s.get(..len as usize)) + .context("pointer out of bounds") +} + impl WasmCtx { - pub fn get_mem(&self) -> Mem { + pub fn get_mem(&self) -> wasmtime::Memory { self.mem.expect("Initialized memory") } - fn mem_env<'a>(ctx: impl Into>) -> (&'a mut MemView, &'a mut Self) { + fn mem_env<'a>(ctx: impl Into>) -> (&'a mut [u8], &'a mut Self) { let ctx = ctx.into(); let mem = ctx.data().get_mem(); - mem.view_and_store_mut(ctx) + mem.data_and_store_mut(ctx) } pub fn bytes_sink_write( @@ -463,9 +469,9 @@ impl WasmCtx { let (mem, env) = Self::mem_env(&mut caller); // Read `buffer_len`, i.e., the capacity of `buffer` pointed to by `buffer_ptr`. - let buffer_len = u32::read_from(mem, buffer_len_ptr)?; + let buffer_len = u32::from_le_bytes(deref_slice(mem, buffer_len_ptr, 4)?.try_into().unwrap()); // Write `buffer` to `sink`. - let buffer = mem.deref_slice(buffer_ptr, buffer_len)?; + let buffer = deref_slice(mem, buffer_ptr, buffer_len)?; env.sink.extend(buffer); Ok(0) diff --git a/crates/cli/src/subcommands/list.rs b/crates/cli/src/subcommands/list.rs index b1d6ad7526f..87f3794e1dc 100644 --- a/crates/cli/src/subcommands/list.rs +++ b/crates/cli/src/subcommands/list.rs @@ -4,7 +4,7 @@ use crate::Config; use clap::{ArgMatches, Command}; use reqwest::StatusCode; use serde::Deserialize; -use spacetimedb::Identity; +use spacetimedb_lib::Identity; use tabled::{ settings::{object::Columns, Alignment, Modify, Style}, Table, Tabled, diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 9d9778041dd..5449627bbb8 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -5,7 +5,7 @@ use base64::{ }; use reqwest::RequestBuilder; use serde::Deserialize; -use spacetimedb::auth::identity::{IncomingClaims, SpacetimeIdentityClaims}; +use spacetimedb_auth::identity::{IncomingClaims, SpacetimeIdentityClaims}; use spacetimedb_client_api_messages::name::{DnsLookupResponse, RegisterTldResult, ReverseDNSResponse}; use spacetimedb_data_structures::map::HashMap; use spacetimedb_lib::{AlgebraicType, Identity}; diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 2cf6fb59808..ad4fd9ffd87 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -15,6 +15,7 @@ path = "src/lib.rs" # The source file of the target. bench = false [dependencies] +spacetimedb-auth.workspace = true spacetimedb-data-structures.workspace = true spacetimedb-lib = { workspace = true, features = ["serde", "metrics_impls"] } spacetimedb-client-api-messages.workspace = true @@ -95,7 +96,6 @@ tokio-util.workspace = true tokio.workspace = true tokio-stream = "0.1" toml.workspace = true -toml_edit.workspace = true tracing-appender.workspace = true tracing-core.workspace = true tracing-flame.workspace = true @@ -107,7 +107,7 @@ url.workspace = true urlencoding.workspace = true uuid.workspace = true wasmtime.workspace = true -jwks = { package = "spacetimedb-jwks", version = "0.1.3" } +jwks.workspace = true async_cache = "0.3.1" faststr = "0.2.23" diff --git a/crates/core/src/auth/mod.rs b/crates/core/src/auth/mod.rs index 73f393aab20..f9c381902e1 100644 --- a/crates/core/src/auth/mod.rs +++ b/crates/core/src/auth/mod.rs @@ -6,7 +6,7 @@ use spacetimedb_paths::cli::{PrivKeyPath, PubKeyPath}; use crate::config::CertificateAuthority; -pub mod identity; +pub use spacetimedb_auth::identity; pub mod token_validation; /// JWT verification and signing keys. diff --git a/crates/core/src/auth/token_validation.rs b/crates/core/src/auth/token_validation.rs index 27b6a786eb5..d9700c739ad 100644 --- a/crates/core/src/auth/token_validation.rs +++ b/crates/core/src/auth/token_validation.rs @@ -157,7 +157,7 @@ impl TokenValidator for DecodingKey { let data = decode::(token, self, &validation)?; let claims = data.claims; - claims.try_into() + claims.try_into().map_err(TokenValidationError::Other) } } diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 79a6c311b42..51aa5cd04da 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -1,9 +1,6 @@ use std::path::Path; use std::{fmt, io}; -use toml; -use toml_edit; - use spacetimedb_lib::Address; use spacetimedb_paths::cli::{ConfigDir, PrivKeyPath, PubKeyPath}; use spacetimedb_paths::server::{ConfigToml, MetadataTomlPath}; @@ -104,50 +101,3 @@ pub struct LogConfig { #[serde(default)] pub directives: Vec, } - -/// Update the value of a key in a `TOML` document, preserving the formatting and comments of the original value. -/// -/// ie: -/// -/// ```toml;no_run -/// # Moving key = value to key = new_value -/// old = "value" # Comment -/// new = "new_value" # Comment -/// ``` -fn copy_value_with_decor(old_value: Option<&toml_edit::Item>, new_value: &str) -> toml_edit::Item { - match old_value { - Some(toml_edit::Item::Value(toml_edit::Value::String(old_value))) => { - // Creates a new `toml_edit::Value` with the same formatting as the old value. - let mut new = toml_edit::Value::String(toml_edit::Formatted::new(new_value.to_string())); - let decor = new.decor_mut(); - // Copy the comments and formatting from the old value. - *decor = old_value.decor().clone(); - new.into() - } - _ => new_value.into(), - } -} - -/// Set the value of a key in a `TOML` document, removing the key if the value is `None`. -/// -/// **NOTE**: This function will preserve the formatting and comments of the original value. -pub fn set_opt_value(doc: &mut toml_edit::DocumentMut, key: &str, value: Option<&str>) { - let old_value = doc.get(key); - if let Some(new) = value { - doc[key] = copy_value_with_decor(old_value, new); - } else { - doc.remove(key); - } -} - -/// Set the value of a key in a `TOML` table, removing the key if the value is `None`. -/// -/// **NOTE**: This function will preserve the formatting and comments of the original value. -pub fn set_table_opt_value(table: &mut toml_edit::Table, key: &str, value: Option<&str>) { - let old_value = table.get(key); - if let Some(new) = value { - table[key] = copy_value_with_decor(old_value, new); - } else { - table.remove(key); - } -} diff --git a/crates/data-structures/Cargo.toml b/crates/data-structures/Cargo.toml index f432dc024a2..d5e52a44e26 100644 --- a/crates/data-structures/Cargo.toml +++ b/crates/data-structures/Cargo.toml @@ -6,7 +6,7 @@ license-file = "LICENSE" description = "Assorted data structures used in spacetimedb" [features] -serde = ["dep:serde"] +serde = ["dep:serde", "hashbrown/serde"] [dependencies] ahash.workspace = true