From 95bc5d9a113523d0775f9794c649da5b64c975ba Mon Sep 17 00:00:00 2001 From: Ryan Butler Date: Mon, 5 Aug 2024 02:20:03 -0400 Subject: [PATCH] no-database endpoints --- Cargo.lock | 43 +++++++++++ Cargo.toml | 4 + apps/identity_server/Cargo.toml | 11 ++- apps/identity_server/src/lib.rs | 7 +- apps/identity_server/src/uuid.rs | 104 +++++++++++++++++++++++++ apps/identity_server/src/v1/mod.rs | 117 ++++++++++++++++++++++++++--- 6 files changed, 275 insertions(+), 11 deletions(-) create mode 100644 apps/identity_server/src/uuid.rs diff --git a/Cargo.lock b/Cargo.lock index 3f1d9d1..7b56358 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4538,13 +4538,20 @@ 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]] @@ -4762,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" @@ -9175,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 a65a5bf..6cec05a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,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" diff --git a/apps/identity_server/Cargo.toml b/apps/identity_server/Cargo.toml index 1aaa756..88bd79c 100644 --- a/apps/identity_server/Cargo.toml +++ b/apps/identity_server/Cargo.toml @@ -12,8 +12,17 @@ publish = false 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 -serde.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 index ec7f7a3..3e6e020 100644 --- a/apps/identity_server/src/lib.rs +++ b/apps/identity_server/src/lib.rs @@ -1,3 +1,4 @@ +mod uuid; pub mod v1; use axum::routing::get; @@ -5,9 +6,13 @@ 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", crate::v1::router()) + .nest("/api/v1", v1_router) .layer(TraceLayer::new_for_http()) } 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 index 45f2f9a..d3c5da8 100644 --- a/apps/identity_server/src/v1/mod.rs +++ b/apps/identity_server/src/v1/mod.rs @@ -1,17 +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 axum::{routing::post, Json, Router}; -use serde::{Deserialize, Serialize}; +use std::{collections::BTreeSet, sync::Arc}; -/// Router of API V1 -pub fn router() -> Router { - Router::new().route("/create", post(create)) +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 create(_pubkey: Json) -> String { - String::from("did:web:todo") +async fn read(_state: State, Path(_user_id): Path) -> Json { + Json(ed25519_pub_jwk( + ed25519::SigningKey::random().verifying_key(), + )) } -#[derive(Debug, Serialize, Deserialize)] -struct JWK; +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" + ); + } +}