diff --git a/Cargo.lock b/Cargo.lock index 7001f54..7b56358 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4533,6 +4533,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "identity_server" +version = "0.0.0" +dependencies = [ + "axum", + "base64 0.21.7", + "clap", + "color-eyre", + "did-simple", + "hex-literal", + "jose-jwk", + "rand 0.8.5", + "serde", + "serde_json", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + [[package]] name = "idna" version = "0.5.0" @@ -4748,6 +4769,39 @@ dependencies = [ "libc", ] +[[package]] +name = "jose-b64" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56" +dependencies = [ + "base64ct", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "jose-jwa" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7" +dependencies = [ + "serde", +] + +[[package]] +name = "jose-jwk" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7" +dependencies = [ + "jose-b64", + "jose-jwa", + "serde", + "zeroize", +] + [[package]] name = "jpeg-decoder" version = "0.3.1" @@ -9161,6 +9215,9 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "serde", +] [[package]] name = "zvariant" diff --git a/Cargo.toml b/Cargo.toml index ec50765..6cec05a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "apps/identity_server", "apps/legacy_web/backend", "apps/legacy_web/frontend", "apps/networked_physics_demo/client", @@ -29,6 +30,7 @@ rust-version = "1.78.0" [workspace.dependencies] async-compat = "0.2.4" +axum = "0.7.5" base64 = "0.21.7" bevy = { version = "0.13", features = ["serialize"] } bevy-inspector-egui = "0.23.4" @@ -47,15 +49,19 @@ bevy_web_asset = { git = "https://github.com/Schmarni-Dev/bevy_web_asset", rev = bytes = "1.5.0" clap = { version = "4.4.11", features = ["derive"] } color-eyre = "0.6" +did-simple.path = "crates/did-simple" egui = "0.26" egui-picking = { path = "crates/egui-picking" } eyre = "0.6" futures = "0.3.30" +hex-literal = "0.4.1" +jose-jwk = { version = "0.1.2", default-features = false } lightyear = "0.12" openxr = "0.18" picking-xr = { path = "crates/picking-xr" } pin-project = "1" rand = "0.8.5" +rand_chacha = "0.3.1" rand_xoshiro = "0.6.0" random-number = "0.1.8" replicate-client.path = "crates/replicate/client" @@ -69,6 +75,7 @@ thiserror = "1.0.56" tokio = { version = "1.35.1", default-features = false } tokio-serde = "0.9" tokio-util = { version = "0.7.10", default-features = true } +tower-http = "0.5.2" tracing = "0.1.40" tracing-subscriber = "0.3.18" url = "2.5.0" diff --git a/apps/identity_server/Cargo.toml b/apps/identity_server/Cargo.toml new file mode 100644 index 0000000..88bd79c --- /dev/null +++ b/apps/identity_server/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "identity_server" +version.workspace = true +license.workspace = true +repository.workspace = true +edition.workspace = true +rust-version.workspace = true +description = "Self-custodial identity using did:web" +publish = false + +[dependencies] +axum.workspace = true +clap.workspace = true +color-eyre.workspace = true +did-simple.workspace = true +jose-jwk = { workspace = true, default-features = false } +rand.workspace = true +serde.workspace = true +serde_json.workspace = true +tokio = { workspace = true, features = ["full"] } +tower-http = { workspace = true, features = ["trace"] } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing.workspace = true +uuid = { workspace = true, features = ["std", "v4", "serde"] } + +[dev-dependencies] +base64.workspace = true +hex-literal.workspace = true diff --git a/apps/identity_server/src/lib.rs b/apps/identity_server/src/lib.rs new file mode 100644 index 0000000..3e6e020 --- /dev/null +++ b/apps/identity_server/src/lib.rs @@ -0,0 +1,21 @@ +mod uuid; +pub mod v1; + +use axum::routing::get; +use tower_http::trace::TraceLayer; + +/// Main router of API +pub fn router() -> axum::Router<()> { + let v1_router = crate::v1::RouterConfig { + ..Default::default() + } + .build(); + axum::Router::new() + .route("/", get(root)) + .nest("/api/v1", v1_router) + .layer(TraceLayer::new_for_http()) +} + +async fn root() -> &'static str { + "uwu hewwo this api is under constwuction" +} diff --git a/apps/identity_server/src/main.rs b/apps/identity_server/src/main.rs new file mode 100644 index 0000000..d1e4429 --- /dev/null +++ b/apps/identity_server/src/main.rs @@ -0,0 +1,33 @@ +use std::net::{Ipv6Addr, SocketAddr}; + +use clap::Parser as _; +use tracing::info; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +#[derive(clap::Parser, Debug)] +struct Cli { + #[clap(default_value = "0")] + port: u16, +} + +#[tokio::main] +async fn main() -> color_eyre::Result<()> { + color_eyre::install()?; + tracing_subscriber::registry() + .with(EnvFilter::try_from_default_env().unwrap_or("info".into())) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let cli = Cli::parse(); + + let listener = tokio::net::TcpListener::bind(SocketAddr::new( + Ipv6Addr::UNSPECIFIED.into(), + cli.port, + )) + .await + .unwrap(); + info!("listening on {}", listener.local_addr().unwrap()); + axum::serve(listener, identity_server::router()) + .await + .map_err(|e| e.into()) +} diff --git a/apps/identity_server/src/uuid.rs b/apps/identity_server/src/uuid.rs new file mode 100644 index 0000000..66e4e06 --- /dev/null +++ b/apps/identity_server/src/uuid.rs @@ -0,0 +1,104 @@ +//! Mockable UUID generation. + +use ::uuid::Uuid; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// Handles generation of UUIDs. This is used instead of the uuid crate directly, +/// to better support deterministic UUID creation in tests. +#[derive(Debug)] +pub struct UuidProvider { + #[cfg(not(test))] + provider: ThreadLocalRng, + #[cfg(test)] + provider: Box, +} + +impl UuidProvider { + #[allow(dead_code)] + pub fn new_thread_local() -> Self { + Self { + #[cfg(test)] + provider: Box::new(ThreadLocalRng), + #[cfg(not(test))] + provider: ThreadLocalRng, + } + } + + /// Allows controlling the sequence of generated UUIDs. Only available in + /// `cfg(test)`. + #[allow(dead_code)] + #[cfg(test)] + pub fn new_from_sequence(uuids: Vec) -> Self { + Self { + provider: Box::new(TestSequence::new(uuids)), + } + } + + #[inline] + pub fn next_v4(&self) -> Uuid { + self.provider.next_v4() + } +} + +impl Default for UuidProvider { + fn default() -> Self { + Self::new_thread_local() + } +} + +trait UuidProviderT: std::fmt::Debug + Send + Sync + 'static { + fn next_v4(&self) -> Uuid; +} + +#[derive(Debug)] +struct ThreadLocalRng; +impl UuidProviderT for ThreadLocalRng { + fn next_v4(&self) -> Uuid { + Uuid::new_v4() + } +} + +/// Provides UUIDs from a known sequence. Useful for tests. +#[derive(Debug)] +struct TestSequence { + uuids: Vec, + pos: AtomicUsize, +} +impl TestSequence { + /// # Panics + /// Panics if len of vec is 0 + #[allow(dead_code)] + fn new(uuids: Vec) -> Self { + assert!(!uuids.is_empty()); + Self { + uuids, + pos: AtomicUsize::new(0), + } + } +} + +impl UuidProviderT for TestSequence { + fn next_v4(&self) -> Uuid { + let curr_pos = self.pos.fetch_add(1, Ordering::SeqCst) % self.uuids.len(); + self.uuids[curr_pos] + } +} + +fn _assert_bounds(p: UuidProvider) { + fn helper(_p: impl std::fmt::Debug + Send + Sync + 'static) {} + helper(p) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_sequence_order() { + let uuids: Vec = (0..4).map(|_| Uuid::new_v4()).collect(); + let sequence = TestSequence::new(uuids.clone()); + for uuid in uuids { + assert_eq!(uuid, sequence.next_v4()); + } + } +} diff --git a/apps/identity_server/src/v1/mod.rs b/apps/identity_server/src/v1/mod.rs new file mode 100644 index 0000000..d3c5da8 --- /dev/null +++ b/apps/identity_server/src/v1/mod.rs @@ -0,0 +1,116 @@ +//! V1 of the API. This is subject to change until we commit to stability, after +//! which point any breaking changes will go in a V2 api. + +use std::{collections::BTreeSet, sync::Arc}; + +use axum::{ + extract::{Path, State}, + response::Redirect, + routing::{get, post}, + Json, Router, +}; +use did_simple::crypto::ed25519; +use jose_jwk::Jwk; +use uuid::Uuid; + +use crate::uuid::UuidProvider; + +#[derive(Debug)] +struct RouterState { + uuid_provider: UuidProvider, +} +type SharedState = Arc; + +/// Configuration for the V1 api's router. +#[derive(Debug, Default)] +pub struct RouterConfig { + pub uuid_provider: UuidProvider, +} + +impl RouterConfig { + pub fn build(self) -> Router { + Router::new() + .route("/create", post(create)) + .route("/users/:id/did.json", get(read)) + .with_state(Arc::new(RouterState { + uuid_provider: self.uuid_provider, + })) + } +} + +async fn create(state: State, _pubkey: Json) -> Redirect { + let uuid = state.uuid_provider.next_v4(); + Redirect::to(&format!("/users/{}/did.json", uuid.as_hyphenated())) +} + +async fn read(_state: State, Path(_user_id): Path) -> Json { + Json(ed25519_pub_jwk( + ed25519::SigningKey::random().verifying_key(), + )) +} + +fn ed25519_pub_jwk(pub_key: ed25519::VerifyingKey) -> jose_jwk::Jwk { + Jwk { + key: jose_jwk::Okp { + crv: jose_jwk::OkpCurves::Ed25519, + x: pub_key.into_inner().as_bytes().as_slice().to_owned().into(), + d: None, + } + .into(), + prm: jose_jwk::Parameters { + ops: Some(BTreeSet::from([jose_jwk::Operations::Verify])), + ..Default::default() + }, + } +} + +#[cfg(test)] +mod test { + use base64::Engine as _; + + use super::*; + + #[test] + fn pub_jwk_test_vectors() { + // See https://datatracker.ietf.org/doc/html/rfc8037#appendix-A.2 + let rfc_example = serde_json::json! ({ + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + }); + let pubkey_bytes = hex_literal::hex!( + "d7 5a 98 01 82 b1 0a b7 d5 4b fe d3 c9 64 07 3a + 0e e1 72 f3 da a6 23 25 af 02 1a 68 f7 07 51 1a" + ); + assert_eq!( + base64::prelude::BASE64_URL_SAFE_NO_PAD + .decode(rfc_example["x"].as_str().unwrap()) + .unwrap(), + pubkey_bytes, + "sanity check: example bytes should match, they come from the RFC itself" + ); + + let input_key = ed25519::VerifyingKey::try_from_bytes(&pubkey_bytes).unwrap(); + let mut output_jwk = ed25519_pub_jwk(input_key); + + // Check all additional outputs for expected values + assert_eq!( + output_jwk.prm.ops.take().unwrap(), + BTreeSet::from([jose_jwk::Operations::Verify]), + "expected Verify as a supported operation" + ); + let output_jwk = output_jwk; // Freeze mutation from here on out + + // Check serialization and deserialization against the rfc example + assert_eq!( + serde_json::from_value::(rfc_example.clone()).unwrap(), + output_jwk, + "deserializing json to Jwk did not match" + ); + assert_eq!( + rfc_example, + serde_json::to_value(output_jwk).unwrap(), + "serializing Jwk to json did not match" + ); + } +}