diff --git a/.gitignore b/.gitignore index ff85536..0cb3449 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ .DS_Store .direnv .envrc + +# SQLite +*.db diff --git a/Cargo.lock b/Cargo.lock index da1b441..6391310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -728,6 +728,15 @@ dependencies = [ "syn 2.0.67", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -2710,8 +2719,8 @@ checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" dependencies = [ "aes-gcm", "base64 0.13.1", - "hkdf", - "hmac", + "hkdf 0.10.0", + "hmac 0.10.1", "percent-encoding", "rand 0.8.5", "sha2 0.9.9", @@ -2817,6 +2826,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -3037,6 +3061,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -3125,7 +3150,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -3211,6 +3238,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -3382,6 +3415,9 @@ name = "either" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +dependencies = [ + "serde", +] [[package]] name = "emath" @@ -3584,6 +3620,17 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "euclid" version = "0.22.10" @@ -3687,6 +3734,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -3792,6 +3850,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.30" @@ -4238,6 +4307,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + [[package]] name = "hassle-rs" version = "0.11.0" @@ -4312,7 +4390,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" dependencies = [ "digest 0.9.0", - "hmac", + "hmac 0.10.1", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", ] [[package]] @@ -4325,6 +4412,15 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "home" version = "0.5.9" @@ -4533,6 +4629,8 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", + "sqlx", + "thiserror", "tokio", "tower-http", "tracing", @@ -4843,6 +4941,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] [[package]] name = "lazycell" @@ -4927,6 +5028,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libudev-sys" version = "0.1.4" @@ -5071,6 +5183,16 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.7.4" @@ -5398,6 +5520,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -5433,6 +5572,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -5881,6 +6031,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -5975,6 +6134,17 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -6637,6 +6807,26 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -7312,6 +7502,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spinning_top" @@ -7341,6 +7534,217 @@ dependencies = [ "der", ] +[[package]] +name = "sqlformat" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" +dependencies = [ + "nom 7.1.3", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27144619c6e5802f1380337a209d2ac1c431002dd74c6e60aebff3c506dc4f0c" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a999083c1af5b5d6c071d34a708a19ba3e02106ad82ef7bbd69f5e48266b613b" +dependencies = [ + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener 5.3.1", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "sha2 0.10.8", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.25.4", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23217eb7d86c584b8cbe0337b9eacf12ab76fe7673c513141ec42565698bb88" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.67", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a099220ae541c5db479c6424bdf1b200987934033c2584f79a0e1693601e776" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.8", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.67", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5afe4c38a9b417b6a9a5eeffe7235d0a106716495536e7727d1c7f4b1ff3eba6" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.5.0", + "byteorder", + "bytes", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf 0.12.4", + "hmac 0.12.1", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1 0.10.6", + "sha2 0.10.8", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1dbb157e65f10dbe01f729339c06d239120221c9ad9fa0ba8408c4cc18ecf21" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.5.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf 0.12.4", + "hmac 0.12.1", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2 0.10.8", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2cdd83c008a622d94499c0006d8ee5f821f36c89b7d625c900e5dc30b5c5ee" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "tracing", + "url", + "uuid", +] + [[package]] name = "standback" version = "0.2.17" @@ -7405,6 +7809,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -7756,6 +8171,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -8013,6 +8439,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -8031,6 +8463,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "universal-hash" version = "0.4.0" @@ -8130,6 +8568,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" @@ -8170,6 +8614,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -8401,6 +8851,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "webpki-roots" version = "0.26.3" @@ -8534,6 +8990,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "whoami" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall 0.4.1", + "wasite", +] + [[package]] name = "wide" version = "0.7.24" diff --git a/apps/identity_server/Cargo.toml b/apps/identity_server/Cargo.toml index 88bd79c..2394ba4 100644 --- a/apps/identity_server/Cargo.toml +++ b/apps/identity_server/Cargo.toml @@ -10,13 +10,15 @@ publish = false [dependencies] axum.workspace = true -clap.workspace = true +clap = { workspace = true, features = ["derive", "env"] } 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 +sqlx = { version = "0.8.0", features = ["runtime-tokio", "tls-rustls", "sqlite", "uuid"] } +thiserror.workspace = true tokio = { workspace = true, features = ["full"] } tower-http = { workspace = true, features = ["trace"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/apps/identity_server/build.rs b/apps/identity_server/build.rs new file mode 100644 index 0000000..0254618 --- /dev/null +++ b/apps/identity_server/build.rs @@ -0,0 +1,4 @@ +fn main() { + // trigger recompilation when a new migration is added + println!("cargo:rerun-if-changed=migrations"); +} diff --git a/apps/identity_server/migrations/20240812062115_users.down.sql b/apps/identity_server/migrations/20240812062115_users.down.sql new file mode 100644 index 0000000..cc1f647 --- /dev/null +++ b/apps/identity_server/migrations/20240812062115_users.down.sql @@ -0,0 +1 @@ +DROP TABLE users; diff --git a/apps/identity_server/migrations/20240812062115_users.up.sql b/apps/identity_server/migrations/20240812062115_users.up.sql new file mode 100644 index 0000000..b892cc7 --- /dev/null +++ b/apps/identity_server/migrations/20240812062115_users.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE "users" +( + user_id BLOB PRIMARY KEY NOT NULL, + pubkeys TEXT NOT NULL +) STRICT; diff --git a/apps/identity_server/src/jwk.rs b/apps/identity_server/src/jwk.rs new file mode 100644 index 0000000..d8f4fc3 --- /dev/null +++ b/apps/identity_server/src/jwk.rs @@ -0,0 +1,71 @@ +use std::collections::BTreeSet; + +use did_simple::crypto::ed25519; +use jose_jwk::Jwk; + +/// Creates a JWK from a ed25519 verifying key. +pub fn ed25519_pub_jwk(pub_key: ed25519::VerifyingKey) -> 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" + ); + } +} diff --git a/apps/identity_server/src/lib.rs b/apps/identity_server/src/lib.rs index 3e6e020..1c86bcb 100644 --- a/apps/identity_server/src/lib.rs +++ b/apps/identity_server/src/lib.rs @@ -1,19 +1,30 @@ -mod uuid; +pub mod jwk; pub mod v1; +mod uuid; + use axum::routing::get; +use color_eyre::eyre::Context as _; use tower_http::trace::TraceLayer; /// Main router of API -pub fn router() -> axum::Router<()> { - let v1_router = crate::v1::RouterConfig { - ..Default::default() +#[derive(Debug, Default)] +pub struct RouterConfig { + pub v1: crate::v1::RouterConfig, +} + +impl RouterConfig { + pub async fn build(self) -> color_eyre::Result> { + let v1 = self + .v1 + .build() + .await + .wrap_err("failed to build v1 router")?; + Ok(axum::Router::new() + .route("/", get(root)) + .nest("/api/v1", v1) + .layer(TraceLayer::new_for_http())) } - .build(); - axum::Router::new() - .route("/", get(root)) - .nest("/api/v1", v1_router) - .layer(TraceLayer::new_for_http()) } async fn root() -> &'static str { diff --git a/apps/identity_server/src/main.rs b/apps/identity_server/src/main.rs index d1e4429..5a4df7f 100644 --- a/apps/identity_server/src/main.rs +++ b/apps/identity_server/src/main.rs @@ -1,13 +1,17 @@ use std::net::{Ipv6Addr, SocketAddr}; use clap::Parser as _; -use tracing::info; +use color_eyre::eyre::Context as _; +use std::path::PathBuf; +use tracing::{info, warn}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; #[derive(clap::Parser, Debug)] struct Cli { - #[clap(default_value = "0")] + #[clap(long, short, default_value = "0")] port: u16, + #[clap(long, env, default_value = "identities.db")] + db_path: PathBuf, } #[tokio::main] @@ -19,6 +23,28 @@ async fn main() -> color_eyre::Result<()> { .init(); let cli = Cli::parse(); + // Validate cli args further + if !cli.db_path.exists() { + warn!( + "no file exists at {}, creating a new database", + cli.db_path.display() + ); + tokio::fs::OpenOptions::new() + .append(true) + .create_new(true) + .open(&cli.db_path) + .await + .wrap_err("failed to create empty file for new database")?; + } + + let v1_cfg = identity_server::v1::RouterConfig { + db_url: format!("sqlite:{}", cli.db_path.display()), + ..Default::default() + }; + let router = identity_server::RouterConfig { v1: v1_cfg } + .build() + .await + .wrap_err("failed to build router")?; let listener = tokio::net::TcpListener::bind(SocketAddr::new( Ipv6Addr::UNSPECIFIED.into(), @@ -27,7 +53,5 @@ async fn main() -> color_eyre::Result<()> { .await .unwrap(); info!("listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, identity_server::router()) - .await - .map_err(|e| e.into()) + axum::serve(listener, router).await.map_err(|e| e.into()) } diff --git a/apps/identity_server/src/v1/mod.rs b/apps/identity_server/src/v1/mod.rs index d3c5da8..f939187 100644 --- a/apps/identity_server/src/v1/mod.rs +++ b/apps/identity_server/src/v1/mod.rs @@ -1,116 +1,145 @@ //! 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 std::sync::Arc; use axum::{ - extract::{Path, State}, - response::Redirect, + extract::{NestedPath, Path, State}, + http::StatusCode, + response::{IntoResponse, Redirect}, routing::{get, post}, Json, Router, }; -use did_simple::crypto::ed25519; -use jose_jwk::Jwk; +use color_eyre::eyre::Context as _; +use jose_jwk::{Jwk, JwkSet}; +use tracing::error; use uuid::Uuid; use crate::uuid::UuidProvider; -#[derive(Debug)] +#[derive(Debug, Clone)] struct RouterState { - uuid_provider: UuidProvider, + uuid_provider: Arc, + db_pool: sqlx::sqlite::SqlitePool, } -type SharedState = Arc; /// Configuration for the V1 api's router. #[derive(Debug, Default)] pub struct RouterConfig { pub uuid_provider: UuidProvider, + pub db_pool_opts: sqlx::sqlite::SqlitePoolOptions, + pub db_url: String, } impl RouterConfig { - pub fn build(self) -> Router { - Router::new() + pub async fn build(self) -> color_eyre::Result { + let db_pool = self + .db_pool_opts + .connect(&self.db_url) + .await + .wrap_err_with(|| { + format!("failed to connect to pool with url {}", self.db_url) + })?; + + sqlx::migrate!("./migrations") + .run(&db_pool) + .await + .wrap_err("failed to run migrations")?; + + Ok(Router::new() .route("/create", post(create)) .route("/users/:id/did.json", get(read)) - .with_state(Arc::new(RouterState { - uuid_provider: self.uuid_provider, + .with_state(RouterState { + uuid_provider: Arc::new(self.uuid_provider), + db_pool, })) } } -async fn create(state: State, _pubkey: Json) -> Redirect { +#[derive(thiserror::Error, Debug)] +enum CreateErr { + #[error(transparent)] + Internal(#[from] color_eyre::Report), +} + +impl IntoResponse for CreateErr { + fn into_response(self) -> axum::response::Response { + error!("{self:?}"); + match self { + Self::Internal(err) => { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response() + } + } + } +} + +#[tracing::instrument(skip_all)] +async fn create( + state: State, + nested_path: NestedPath, + pubkey: Json, +) -> Result { let uuid = state.uuid_provider.next_v4(); - Redirect::to(&format!("/users/{}/did.json", uuid.as_hyphenated())) + let jwks = JwkSet { + keys: vec![pubkey.0], + }; + let serialized_jwks = serde_json::to_string(&jwks).expect("infallible"); + + sqlx::query("INSERT INTO users (user_id, pubkeys) VALUES ($1, $2)") + .bind(uuid) + .bind(serialized_jwks) + .execute(&state.db_pool) + .await + .wrap_err("failed to insert identity into db")?; + + Ok(Redirect::to(&format!( + "{}/users/{}/did.json", + nested_path.as_str(), + uuid.as_hyphenated() + ))) } -async fn read(_state: State, Path(_user_id): Path) -> Json { - Json(ed25519_pub_jwk( - ed25519::SigningKey::random().verifying_key(), - )) +#[derive(thiserror::Error, Debug)] +enum ReadErr { + #[error("no such user exists")] + NoSuchUser, + #[error(transparent)] + Internal(#[from] color_eyre::Report), } -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, +impl IntoResponse for ReadErr { + fn into_response(self) -> axum::response::Response { + error!("{self:?}"); + match self { + ReadErr::NoSuchUser => { + (StatusCode::NOT_FOUND, self.to_string()).into_response() + } + Self::Internal(err) => { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response() + } } - .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" - ); - } +// TODO: currently this returns a JSON Web Key Set, but we actually want to be +// returning a did:web json. +#[tracing::instrument(skip_all)] +async fn read( + state: State, + Path(user_id): Path, +) -> Result, ReadErr> { + let keyset_in_string: Option = + sqlx::query_scalar("SELECT pubkeys FROM users WHERE user_id = $1") + .bind(user_id) + .fetch_optional(&state.db_pool) + .await + .wrap_err("failed to retrieve from database")?; + let Some(keyset_in_string) = keyset_in_string else { + return Err(ReadErr::NoSuchUser); + }; + // TODO: Do we actually care about round-trip validating the JwkSet here? + let keyset: JwkSet = serde_json::from_str(&keyset_in_string) + .wrap_err("failed to deserialize JwkSet from database")?; + + Ok(Json(keyset)) }